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
- What is Hexagonal Architecture?
- Project Structure
- Key Design Principles
- Technologies Used
- Getting Started
- API Documentation
- Testing
- Benefits of This Architecture
ποΈ 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 β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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
-
Domain (Core/Hexagon): The business logic lives here, completely isolated from external concerns
- Pure business rules
- Domain models
- Domain exceptions
- No framework dependencies
-
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)
-
Input Ports: Define use cases (e.g.,
-
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
π 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
2. Start PostgreSQL Database
docker-compose up -d
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
Or using Maven:
mvn spring-boot:run
The application will start on http://localhost:8080
4. Verify the Application
curl http://localhost:8080/api/tasks
You should receive an empty array [] (no tasks yet).
π API Documentation
Base URL
http://localhost:8080/api/tasks
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"
}
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"
}
]
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"
}
4. Update Task
PUT /api/tasks/{id}
Content-Type: application/json
{
"title": "Master Hexagonal Architecture",
"description": "Deep dive into advanced concepts"
}
Response: 200 OK
5. Change Task Status
PATCH /api/tasks/{id}/status
Content-Type: application/json
{
"status": "IN_PROGRESS"
}
Response: 200 OK
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
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"
}
]
}
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"
}
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"
}
π§ͺ Testing
Run All Tests
./mvnw test
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());
}
}
Swapping Database (PostgreSQL β MongoDB)
Just create a new adapter implementing TaskRepository:
@Repository
public class MongoTaskRepository implements TaskRepository {
private final MongoTemplate mongoTemplate;
// Implementation using MongoDB
}
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
π Environment Variables
Copy .env.example to .env and customize if needed:
cp .env.example .env
Default values:
POSTGRES_DB=taskdbPOSTGRES_USER=taskuserPOSTGRES_PASSWORD=taskpassPOSTGRES_PORT=5432
π Further Reading
- Hexagonal Architecture by Alistair Cockburn
- Clean Architecture by Robert C. Martin
- Ports and Adapters Pattern
Built with β€οΈ to demonstrate Hexagonal Architecture principles
Top comments (0)