Hexagonal Architecture (Ports & Adapters)
The application core is isolated from infrastructure through ports (interfaces) and adapters (implementations), making domain logic independently testable.
What it is
Hexagonal Architecture, coined by Alistair Cockburn in 2005, organizes an application around its business domain, which sits at the center (the "hexagon"). The domain interacts with the outside world only through well-defined interfaces called ports. Actual implementations — REST controllers, database repositories, email clients, message queue consumers — are adapters that plug into those ports. Primary ports (or "driving" ports) represent the application's use cases and are called by external actors like HTTP clients or CLI tools. Secondary ports (or "driven" ports) represent infrastructure the application depends on, such as persistence, messaging, and external APIs. The key rule: the domain and application core depend on ports (abstractions), never on concrete adapters (infrastructure). This is a direct application of the Dependency Inversion Principle.
Why it exists
Traditional layered architectures still allow business logic to leak into controllers and repositories because there is no structural enforcement against it. Hexagonal Architecture provides that enforcement: the domain layer is not allowed to import infrastructure code. The primary benefit is testability — the entire domain can be tested with fast, in-memory fakes (implementing the secondary ports) without spinning up databases, message brokers, or HTTP servers. This leads to test suites that run in seconds rather than minutes, giving developers rapid feedback. It also dramatically simplifies infrastructure swaps: changing from PostgreSQL to DynamoDB means writing a new adapter, not touching the domain. Cockburn framed it as allowing the application to be driven equally by tests, users, and programs — all through ports.
When to use
- Business logic is complex enough to warrant isolation from infrastructure concerns.
- Fast, in-process unit tests of domain logic are a priority.
- The application may need to swap or support multiple infrastructure backends (e.g., multiple notification channels, pluggable storage backends).
- The team practices TDD — hexagonal is the natural structural home for test-driven domain modeling.
- Long-lived codebases where infrastructure evolves (cloud migrations, framework upgrades) but business rules stay stable.
When not to use
- Simple CRUD applications with little business logic — the overhead of ports and adapters adds structure without value when there's no domain to protect.
- Prototype or throwaway code where speed of initial creation matters more than long-term maintainability.
- Very small teams new to the pattern may find the indirection confusing before the benefits materialize.
Typical architecture
The hexagon represents the application core. Left-side adapters drive the application (REST, CLI, tests). Right-side adapters are driven by the application (database, email, message broker).
[REST Controller] [CLI] [Test]
│ │ │
(Primary Adapters — Driving)
│ │ │
┌─────▼──────────────▼──────▼──────┐
│ Primary Ports │
│ ┌────────────────────────────┐ │
│ │ Application Core │ │
│ │ (Domain + Use Cases) │ │
│ └────────────────────────────┘ │
│ Secondary Ports │
└──┬───────────┬──────────┬─────────┘
│ │ │
(Secondary Adapters — Driven)
│ │ │
[PostgreSQL [RabbitMQ [Stripe
Adapter] Adapter] Adapter]
Pros and cons
Pros
- Domain logic is fully testable without any infrastructure — tests run in milliseconds.
- Infrastructure is replaceable: swap the database adapter without touching domain code.
- Forces explicit modeling of what the application does (ports) vs. how it does it (adapters).
- Makes it possible to run the application from tests, a CLI, or HTTP with no code changes to the core.
- Natural alignment with DDD — the domain hexagon maps to a bounded context.
Cons
- Boilerplate — every infrastructure dependency requires a port interface plus at least one adapter implementation.
- More files and indirection; can feel over-engineered for simple domains.
- Risk of anemic domain models if use cases are modeled as orchestration scripts rather than rich domain behavior.
- Learning curve — developers unfamiliar with dependency inversion may struggle to identify what belongs in the core vs. adapters.
Implementation notes
Define secondary port interfaces in the domain package — not in the infrastructure package. This forces infrastructure code to depend on the domain, not the reverse. Create an in-memory adapter for each secondary port to use in unit tests, and a real adapter for production use. Map between domain objects and infrastructure DTOs at the adapter boundary — never let ORM entities or API response DTOs leak into the domain. Use dependency injection to wire adapters to ports at startup (composition root), keeping the domain free of any DI framework dependency. Group code by feature (vertical slice) rather than by layer to keep related ports and adapters discoverable together.
Common failure modes
- Infrastructure leaking into the domain: JPA annotations, Spring annotations, or Kafka-specific types appearing in domain classes — the most common violation.
- Anemic domain model: All business logic ends up in use case interactors while domain objects are just data holders, missing the "rich domain" benefit.
- Port interface explosion: A separate interface for every single repository method, leading to dozens of rarely-reused port interfaces.
- Missing in-memory adapters: Tests use the real database adapter, making them slow and fragile; the primary testability benefit is lost.
- Adapter leakage in tests: Tests import adapter classes directly instead of using ports, breaking the abstraction.
Decision checklist
- Are all secondary port interfaces defined in the domain/application layer, not in infrastructure?
- Can you run a full domain test suite without starting a database, broker, or HTTP server?
- Are in-memory adapter implementations available for all secondary ports?
- Do adapters translate between infrastructure-specific types and domain types at the boundary?
- Is there a composition root that wires adapters to ports, keeping the domain free of DI annotations?
- Are domain objects free of all framework and infrastructure imports?
Example use cases
- Banking domain services: Account transfer logic tested entirely in memory, with a PostgreSQL adapter for production and an in-memory adapter for tests — the domain doesn't know or care.
- Multi-channel notification service: A notification port with SMS, email, and push adapters; the domain calls the port, and the DI container wires the appropriate adapter per environment.
- E-commerce checkout: The checkout use case depends on a PaymentGatewayPort; in tests, a test double fulfills the port; in production, a Stripe adapter does.
Related patterns
- Clean Architecture — Uncle Bob's concentric-ring formulation of similar dependency inversion principles.
- Onion Architecture — Jeffrey Palermo's variant with explicit domain service and application service layers.
- CQRS — Often applied within the hexagonal core, with separate command and query ports.
Further reading
- Hexagonal Architecture — Alistair Cockburn's original article.
- Hexagonal Architecture with Java and Spring — Reflectoring.io walkthrough.
- Ready for Changes with Hexagonal Architecture — Netflix Tech Blog.