DEV Community

rezahazegh
rezahazegh

Posted on

Hexagonal Architecture Demo - Task Management System

A Spring Boot application demonstrating Hexagonal Architecture (also known as Ports and Adapters) with a Task Management system. This project showcases clean separation of concerns, dependency inversion, and testable code structure.

πŸ“„ Repository

https://github.com/rezahazegh/demo-hexagonal-architecture

πŸ“‹ Table of Contents

πŸ—οΈ Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Infrastructure Layer                      β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚              Inbound Adapters (Input)                β”‚   β”‚
β”‚  β”‚         REST API Controllers & DTOs                   β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                     β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚              Application Layer                        β”‚   β”‚
β”‚  β”‚        Use Cases & Business Orchestration             β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                     β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                Domain Layer (Core)                    β”‚   β”‚
β”‚  β”‚        Business Logic & Domain Models                 β”‚   β”‚
β”‚  β”‚              No External Dependencies                 β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                     β”‚                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚             Outbound Adapters (Output)                β”‚   β”‚
β”‚  β”‚      PostgreSQL Repository & Persistence              β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

Layer Interaction Flow

REST Request β†’ Controller β†’ Application Service β†’ Domain Logic β†’ Repository β†’ Database

The dependency rule: Dependencies point inward (Infrastructure β†’ Application β†’ Domain)

🎯 What is Hexagonal Architecture?

Hexagonal Architecture (coined by Alistair Cockburn) is a software design pattern that creates loosely coupled application components.

Core Concepts

  1. Domain (Core/Hexagon): The business logic lives here, completely isolated from external concerns

    • Pure business rules
    • Domain models
    • Domain exceptions
    • No framework dependencies
  2. Ports: Interfaces that define contracts

    • Input Ports: Define use cases (e.g., TaskService)
    • Output Ports: Define what the domain needs from external systems (e.g., TaskRepository)
  3. Adapters: Implementations that connect to the outside world

    • Inbound/Primary Adapters: Drive the application (e.g., REST controllers, CLI)
    • Outbound/Secondary Adapters: Driven by the application (e.g., database repositories, external APIs)

Why "Hexagonal"?

The hexagon shape symbolizes that the architecture can have multiple adapters on each side - it's not limited to just one input or output method.

πŸ“ Project Structure

dev.hazegh.demo_hexagonal_architecture/
β”‚
β”œβ”€β”€ domain/                                    # πŸ”΅ CORE DOMAIN (No external dependencies)
β”‚   β”œβ”€β”€ model/
β”‚   β”‚   β”œβ”€β”€ Task.java                         # Domain entity with business logic
β”‚   β”‚   └── TaskStatus.java                   # Enum (TODO, IN_PROGRESS, DONE)
β”‚   β”œβ”€β”€ exception/
β”‚   β”‚   β”œβ”€β”€ TaskNotFoundException.java
β”‚   β”‚   └── InvalidTaskStateException.java
β”‚   └── port/
β”‚       └── output/
β”‚           └── TaskRepository.java           # Output port interface
β”‚
β”œβ”€β”€ application/                               # 🟒 APPLICATION LAYER
β”‚   β”œβ”€β”€ port/
β”‚   β”‚   └── input/
β”‚   β”‚       └── TaskService.java              # Input port (use case interface)
β”‚   └── service/
β”‚       └── TaskServiceImpl.java              # Use case implementation
β”‚
└── infrastructure/                            # 🟠 INFRASTRUCTURE LAYER
    └── adapter/
        β”œβ”€β”€ input/
        β”‚   └── rest/
        β”‚       β”œβ”€β”€ TaskController.java       # REST API adapter
        β”‚       β”œβ”€β”€ dto/
        β”‚       β”‚   β”œβ”€β”€ CreateTaskRequest.java
        β”‚       β”‚   β”œβ”€β”€ UpdateTaskRequest.java
        β”‚       β”‚   β”œβ”€β”€ ChangeStatusRequest.java
        β”‚       β”‚   └── TaskResponse.java
        β”‚       └── exception/
        β”‚           β”œβ”€β”€ GlobalExceptionHandler.java
        β”‚           └── ErrorResponse.java
        └── output/
            └── persistence/
                β”œβ”€β”€ entity/
                β”‚   └── TaskJpaEntity.java    # JPA entity (infrastructure concern)
                β”œβ”€β”€ mapper/
                β”‚   └── TaskMapper.java       # Maps between domain and JPA
                β”œβ”€β”€ TaskJpaRepository.java    # Spring Data JPA interface
                └── TaskRepositoryAdapter.java # Implements domain port
Enter fullscreen mode Exit fullscreen mode

πŸ”‘ Key Design Principles

1. Dependency Inversion Principle

  • High-level modules (domain) don't depend on low-level modules (infrastructure)
  • Both depend on abstractions (ports/interfaces)

2. Separation of Concerns

  • Domain: Business rules (e.g., "A completed task cannot go back to TODO")
  • Application: Orchestration (e.g., "Find task, change status, save task")
  • Infrastructure: Technical details (e.g., REST, JPA, PostgreSQL)

3. Testability

  • Domain logic can be tested without any frameworks
  • Application logic can be tested with mocked repositories
  • Each layer can be tested independently

4. Flexibility

  • Easy to swap adapters (e.g., PostgreSQL β†’ MongoDB, REST β†’ GraphQL)
  • Domain remains unchanged when infrastructure changes

πŸ› οΈ Technologies Used

  • Java 21
  • Spring Boot 4.0.1
    • Spring Web (REST API)
    • Spring Data JPA (Persistence)
    • Spring Validation (Bean validation)
  • PostgreSQL (Production database)
  • H2 (Test database)
  • Lombok (Reduce boilerplate)
  • Maven (Build tool)
  • Docker & Docker Compose (Containerization)
  • JUnit 5 & Mockito (Testing)

πŸš€ Getting Started

Prerequisites

  • Java 21 or higher
  • Docker and Docker Compose
  • Maven 3.9+ (or use the included Maven wrapper)

1. Clone the Repository

git clone https://github.com/rezahazegh/demo-hexagonal-architecture
cd demo-hexagonal-architecture
Enter fullscreen mode Exit fullscreen mode

2. Start PostgreSQL Database

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

This will start PostgreSQL on localhost:5432 with:

  • Database: taskdb
  • Username: taskuser
  • Password: taskpass

3. Run the Application

Using Maven Wrapper (Recommended):

./mvnw spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Or using Maven:

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

The application will start on http://localhost:8080

4. Verify the Application

curl http://localhost:8080/api/tasks
Enter fullscreen mode Exit fullscreen mode

You should receive an empty array [] (no tasks yet).

πŸ“š API Documentation

Base URL

http://localhost:8080/api/tasks
Enter fullscreen mode Exit fullscreen mode

Endpoints

1. Create a Task

POST /api/tasks
Content-Type: application/json

{
  "title": "Learn Hexagonal Architecture",
  "description": "Study the concepts and implement a demo project"
}

Response: 201 Created
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "Learn Hexagonal Architecture",
  "description": "Study the concepts and implement a demo project",
  "status": "TODO",
  "createdAt": "2025-12-26T10:00:00",
  "updatedAt": "2025-12-26T10:00:00"
}
Enter fullscreen mode Exit fullscreen mode

2. Get All Tasks

GET /api/tasks

Response: 200 OK
[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "title": "Learn Hexagonal Architecture",
    "description": "Study the concepts and implement a demo project",
    "status": "TODO",
    "createdAt": "2025-12-26T10:00:00",
    "updatedAt": "2025-12-26T10:00:00"
  }
]
Enter fullscreen mode Exit fullscreen mode

3. Get Task by ID

GET /api/tasks/{id}

Response: 200 OK
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "title": "Learn Hexagonal Architecture",
  "description": "Study the concepts and implement a demo project",
  "status": "TODO",
  "createdAt": "2025-12-26T10:00:00",
  "updatedAt": "2025-12-26T10:00:00"
}
Enter fullscreen mode Exit fullscreen mode

4. Update Task

PUT /api/tasks/{id}
Content-Type: application/json

{
  "title": "Master Hexagonal Architecture",
  "description": "Deep dive into advanced concepts"
}

Response: 200 OK
Enter fullscreen mode Exit fullscreen mode

5. Change Task Status

PATCH /api/tasks/{id}/status
Content-Type: application/json

{
  "status": "IN_PROGRESS"
}

Response: 200 OK
Enter fullscreen mode Exit fullscreen mode

Valid status transitions:

  • TODO β†’ IN_PROGRESS βœ…
  • TODO β†’ DONE βœ…
  • IN_PROGRESS β†’ DONE βœ…
  • DONE β†’ TODO ❌ (Business rule violation)
  • DONE β†’ IN_PROGRESS ❌ (Business rule violation)

6. Delete Task

DELETE /api/tasks/{id}

Response: 204 No Content
Enter fullscreen mode Exit fullscreen mode

Error Responses

Validation Error

{
  "timestamp": "2025-12-26T10:00:00",
  "status": 400,
  "error": "Validation Failed",
  "message": "Invalid input data",
  "path": "/api/tasks",
  "validationErrors": [
    {
      "field": "title",
      "message": "Title is required"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Not Found

{
  "timestamp": "2025-12-26T10:00:00",
  "status": 404,
  "error": "Not Found",
  "message": "Task not found with id: 550e8400-e29b-41d4-a716-446655440000",
  "path": "/api/tasks/550e8400-e29b-41d4-a716-446655440000"
}
Enter fullscreen mode Exit fullscreen mode

Invalid State Transition

{
  "timestamp": "2025-12-26T10:00:00",
  "status": 400,
  "error": "Bad Request",
  "message": "Failed to change status: Cannot move a completed task back to TODO",
  "path": "/api/tasks/550e8400-e29b-41d4-a716-446655440000/status"
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ Testing

Run All Tests

./mvnw test
Enter fullscreen mode Exit fullscreen mode

Test Structure

Unit Tests

  • Domain Tests (TaskTest.java): Test business logic in isolation
  • Application Tests (TaskServiceImplTest.java): Test use cases with mocked dependencies
  • Mapper Tests (TaskMapperTest.java): Test data transformations

Integration Tests

  • REST API Tests (TaskControllerIntegrationTest.java): End-to-end API testing
  • Repository Tests (TaskRepositoryAdapterTest.java): Database integration testing

Test Coverage

The tests demonstrate:

  • βœ… Business rule enforcement (e.g., status transition rules)
  • βœ… Validation handling
  • βœ… Error scenarios (not found, invalid state)
  • βœ… CRUD operations
  • βœ… Mapper bidirectional conversions
  • βœ… Repository persistence

πŸ’‘ Benefits of This Architecture

1. Independence from Frameworks

  • The core business logic doesn't depend on Spring, JPA, or any framework
  • Frameworks become implementation details

2. Testability

  • Domain logic can be tested without any infrastructure
  • Easy to write unit tests with minimal setup
  • Clear boundaries make mocking straightforward

3. Flexibility

  • Database change: Swap PostgreSQL for MongoDB without touching domain/application
  • API change: Add GraphQL alongside REST without modifying business logic
  • Add new adapters: CLI, gRPC, message queue - all without changing the core

4. Maintainability

  • Clear separation makes code easier to understand
  • Changes in one layer don't ripple through the entire application
  • Each component has a single responsibility

5. Business Logic Protection

  • Domain rules are explicit and enforced in one place
  • No risk of bypassing business logic through different entry points

6. Team Scalability

  • Different teams can work on different layers independently
  • Clear contracts (ports) reduce integration conflicts

πŸ”„ Example: Swapping Adapters

Current Setup

  • REST API (Inbound)
  • PostgreSQL (Outbound)

Adding a New Adapter (e.g., CLI)

You can add a CLI adapter without changing domain or application:

@Component
public class TaskCliAdapter implements CommandLineRunner {
    private final TaskService taskService;

    @Override
    public void run(String... args) {
        Task task = taskService.createTask("CLI Task", "Created from CLI");
        System.out.println("Created task: " + task.getId());
    }
}
Enter fullscreen mode Exit fullscreen mode

Swapping Database (PostgreSQL β†’ MongoDB)

Just create a new adapter implementing TaskRepository:

@Repository
public class MongoTaskRepository implements TaskRepository {
    private final MongoTemplate mongoTemplate;
    // Implementation using MongoDB
}
Enter fullscreen mode Exit fullscreen mode

The domain and application layers remain unchanged!

🐳 Docker Commands

# Start PostgreSQL
docker-compose up -d

# Stop PostgreSQL
docker-compose down

# View logs
docker-compose logs -f postgres

# Clean up (removes volumes)
docker-compose down -v
Enter fullscreen mode Exit fullscreen mode

πŸ“ Environment Variables

Copy .env.example to .env and customize if needed:

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

Default values:

  • POSTGRES_DB=taskdb
  • POSTGRES_USER=taskuser
  • POSTGRES_PASSWORD=taskpass
  • POSTGRES_PORT=5432

πŸ“– Further Reading


Built with ❀️ to demonstrate Hexagonal Architecture principles

Top comments (0)