Skip to content

Hexagonal / Clean Architecture

Status: 🟢 Active  |  Owner: Architecture Team  |  Last Reviewed: 2025-Q4


Overview

Hexagonal Architecture (also called Ports and Adapters, or Clean Architecture in Robert Martin's formulation) is the recommended internal architecture for all services beyond simple CRUD APIs. It produces services that are independently testable at each layer, where the core business logic has no knowledge of the frameworks, databases, or external systems that surround it.

The central insight is simple: business logic should not depend on infrastructure. Infrastructure (databases, HTTP frameworks, message brokers, external APIs) should depend on the business logic — never the reverse.


The Dependency Rule

All source code dependencies must point inward — toward higher-level policies.

┌─────────────────────────────────────────────────┐
│                  Infrastructure                  │
│   (HTTP, DB, Messaging, External APIs, CLI)      │
│  ┌───────────────────────────────────────────┐  │
│  │              Application                  │  │
│  │         (Use Cases / Orchestration)       │  │
│  │  ┌─────────────────────────────────────┐  │  │
│  │  │              Domain                 │  │  │
│  │  │  (Entities, Value Objects, Domain   │  │  │
│  │  │   Services, Repository Interfaces)  │  │  │
│  │  └─────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘
         Dependencies flow INWARD only →

Layers and Responsibilities

Domain Layer (innermost)

The core of the application. Contains: - Entities — objects with identity and lifecycle. - Value Objects — immutable, identity-free domain concepts. - Aggregates — clusters of entities with an Aggregate Root. - Domain Services — stateless operations that don't belong to a single entity. - Domain Events — records of significant domain occurrences. - Repository Interfaces (Ports) — interfaces the domain defines to describe what it needs from storage. Implementations live in infrastructure.

Rules: - No framework dependencies. No Spring, no Django, no Express annotations/decorators. - No I/O. No database calls, no HTTP calls, no file access. - Fully unit-testable with no mocking of infrastructure.

Application Layer

Orchestrates domain objects to implement use cases. Contains: - Application Services / Use Case handlers — one class per use case. - DTOs — Data Transfer Objects for input/output across the application boundary. - Port interfaces for infrastructure the application needs (email, file storage, event publishing).

Rules: - Depends on the domain layer. Never on infrastructure. - Transactions are managed at this layer. - No business logic — that belongs in the domain. The application layer orchestrates; the domain decides.

Infrastructure Layer (outermost)

Implements all interfaces defined in the domain and application layers. Contains: - Repository implementations (JPA, SQL, MongoDB adapters). - HTTP controllers (REST endpoints, GraphQL resolvers). - Messaging adapters (Kafka consumers/producers, SQS handlers). - External service clients (third-party API adapters). - Framework configuration (Spring Boot, FastAPI setup, NestJS modules).

Rules: - Depends on the application and domain layers. - Implements ports defined by inner layers — it is the adapter half of "Ports and Adapters". - Should contain no business logic.


Standard Project Structure

src/main/java/com/acme/orders/
├── domain/
│   ├── model/
│   │   ├── Order.java                  # Aggregate Root
│   │   ├── OrderItem.java              # Entity
│   │   └── Money.java                  # Value Object
│   ├── service/
│   │   └── OrderPricingService.java    # Domain Service
│   ├── event/
│   │   └── OrderPlacedEvent.java       # Domain Event
│   └── repository/
│       └── OrderRepository.java        # Port (interface)
├── application/
│   ├── PlaceOrderUseCase.java          # Use Case
│   ├── CancelOrderUseCase.java
│   └── dto/
│       ├── PlaceOrderCommand.java      # Input DTO
│       └── OrderSummaryDto.java        # Output DTO
└── infrastructure/
    ├── persistence/
    │   ├── JpaOrderRepository.java     # Adapter (implements port)
    │   └── OrderJpaEntity.java         # JPA-specific mapping
    ├── http/
    │   └── OrderController.java        # REST adapter
    ├── messaging/
    │   └── OrderEventPublisher.java    # Kafka adapter
    └── config/
        └── ApplicationConfig.java
src/
├── domain/
│   ├── model/
│   │   ├── order.py                    # Aggregate + entities
│   │   └── money.py                    # Value Object
│   ├── service/
│   │   └── pricing_service.py
│   └── repository.py                   # Port (Protocol/ABC)
├── application/
│   ├── place_order.py                  # Use Case
│   └── dto/
│       └── order_dto.py
└── infrastructure/
    ├── persistence/
    │   └── sql_order_repository.py     # Adapter
    ├── http/
    │   └── order_router.py             # FastAPI router
    └── messaging/
        └── kafka_event_publisher.py
src/
├── domain/
│   ├── entities/
│   │   └── order.entity.ts
│   ├── value-objects/
│   │   └── money.value-object.ts
│   └── repositories/
│       └── order.repository.ts         # Interface (Port)
├── application/
│   ├── use-cases/
│   │   └── place-order.use-case.ts
│   └── dto/
│       └── place-order.dto.ts
└── infrastructure/
    ├── persistence/
    │   └── typeorm-order.repository.ts # Adapter
    ├── http/
    │   └── order.controller.ts
    └── messaging/
        └── kafka-event-publisher.ts

Ports and Adapters

The architecture gets its alternative name from the pattern of defining Ports (interfaces) for everything the domain needs to communicate with outside itself, and Adapters that implement those ports for specific technologies.

Primary (Driving) Ports

Interfaces through which the outside world drives the application. HTTP controllers, CLI commands, event consumers, and scheduled jobs are all primary adapters that call into application services.

Secondary (Driven) Ports

Interfaces the application calls to interact with infrastructure. Repository interfaces, email service interfaces, and event publisher interfaces are secondary ports. The infrastructure layer provides the adapters that implement them.


Testing Strategy

Hexagonal Architecture makes each layer independently testable:

Layer Test Type Dependencies
Domain Pure unit tests None — no mocks needed
Application Unit tests with mocked ports Mock repositories and external services
Infrastructure Integration tests Real DB, real message broker (or TestContainers)
Full stack E2E / acceptance tests Full application stack
// Domain layer test — pure, no mocks
@Test
void order_total_is_sum_of_all_items() {
    Order order = new Order(customerId);
    order.addItem(product("SKU-1", Money.of(10, GBP)), 2);
    order.addItem(product("SKU-2", Money.of(5, GBP)), 1);

    assertThat(order.getTotal()).isEqualTo(Money.of(25, GBP));
}

// Application layer test — mock the port
@Test
void placing_order_saves_and_publishes_event() {
    OrderRepository repository = mock(OrderRepository.class);
    EventPublisher publisher = mock(EventPublisher.class);
    PlaceOrderUseCase useCase = new PlaceOrderUseCase(repository, publisher);

    useCase.execute(new PlaceOrderCommand(customerId, items));

    verify(repository).save(any(Order.class));
    verify(publisher).publish(any(OrderPlacedEvent.class));
}

When to Use a Simpler Architecture

Hexagonal Architecture introduces real structure — it is not appropriate for every service. Use a simpler layered or flat architecture for:

  • Simple CRUD APIs with no business logic.
  • Short-lived scripts and data pipelines.
  • Services with < 500 lines of production code.

Use Hexagonal Architecture for: - Services with non-trivial business rules. - Services expected to grow and evolve significantly. - Services where database or framework replacement is a realistic possibility. - Any service where testability of the domain logic is important.


References


Last reviewed: 2025-Q4  |  Owner: Architecture Team