Hexagonal#
Hexagonal architecture (Alistair Cockburn, 2005), also known as Ports and Adapters, separates the application’s core from the outside world. The core defines abstract interfaces (ports) for everything it needs; concrete code (adapters) plugs the outside world into those ports.
The same idea, with slightly different forms, appears as Onion Architecture (Jeffrey Palermo) and Clean Architecture (Uncle Bob). Treat them as variations on a theme.
Structure#
flowchart LR
subgraph Driving["Driving (Primary) Adapters"]
HTTP[HTTP Controller]
CLI[CLI Handler]
Worker[Queue Worker]
end
subgraph Core["Core: Application + Domain"]
UC[Use Cases]
Dom[Domain Model]
UC --- Dom
end
subgraph Driven["Driven (Secondary) Adapters"]
PG[Postgres Repo]
Kafka[Kafka Publisher]
SES[SES Mailer]
end
HTTP --> UC
CLI --> UC
Worker --> UC
UC --> PG
UC --> Kafka
UC --> SES
Inverted dependencies: the core doesn’t know about the adapters, and nothing in the core mentions HTTP / SQL / Kafka.
Why#
The four payoffs that justify the extra structure. Testability is the headline; replaceability and long-lived domain isolation are the structural wins; the discipline-forced clarity is the under-counted reason teams stick with the pattern.
Testability, the core runs in tests with fake adapters. No database in unit tests.
Replaceability, swap Postgres for DynamoDB by writing a new adapter. The core doesn’t change.
Long-lived domain, the parts that change least slowly (business rules) are insulated from the parts that churn most (HTTP frameworks, ORMs, message brokers).
Forced clarity, you can’t accidentally couple business logic to the HTTP request format.
The Three Layers (in practice)#
Modern interpretations usually split the core into two layers (domain plus application) wrapped by a third (infrastructure). The strict rule is that dependencies always point inward; infrastructure depends on application; application depends on domain; domain depends on nothing.
Domain, entities, value objects, domain services, business invariants. Pure. No knowledge of the world.
Application, use cases that orchestrate the domain. Defines ports.
Infrastructure / Adapters, HTTP handlers, database repositories, message consumers. Implement ports.
Dependencies point inward: infrastructure depends on application; application depends on domain; domain depends on nothing.
Ports and Adapters#
The vocabulary that gives the pattern its name. Driving ports are what the world calls the application; driven ports are what the application asks of the world. Adapters implement either side and live in infrastructure.
Driving (primary) ports, “the application’s API to the outside”. Use cases / commands / queries. The HTTP adapter calls these.
Driven (secondary) ports, “what the application asks of the outside”. Repository, mailer, clock, ID generator. Adapters implement these.
Pseudocode.
// domain
class Order { ... }
// application
interface OrderRepository { // driven port
save(o: Order): void
findById(id: ID): Order?
}
class PlaceOrder { // use case (driving port)
constructor(repo: OrderRepository, clock: Clock) { ... }
execute(input: PlaceOrderInput): OrderId { ... }
}
// infrastructure (adapters)
class PostgresOrderRepository implements OrderRepository { ... }
class HttpOrderController { /* calls PlaceOrder.execute */ }
Tests#
The classic payoff. Hexagonal lines up well with the test pyramid: most behavior is exercised cheaply at the application/domain layer with fakes; only the adapters need real I/O to test, and end-to-end runs are reserved for the few high-value flows.
Unit tests on use cases with in-memory adapters.
Integration tests on adapters against real infrastructure (Testcontainers).
End-to-end tests through the HTTP entry point.
Most behaviors get unit-tested at the application/domain level; only the adapters need real I/O.
Strengths#
What hexagonal earns its keep on. The domain is insulated from framework churn; the test pyramid is fast and clean; new adapters (CLI, queue worker, GraphQL) plug into the same use cases without duplicating logic.
Long-lived business logic that survives framework churn.
Clear test pyramid; fast inner loop.
Easy onboarding; the domain reads as plain code.
Multiple adapters for the same port (HTTP API + CLI + scheduled job).
Costs#
What you pay for the structure. The biggest risks are over- engineering small applications and the slow drift toward an anemic domain when business rules quietly migrate into “service” layers; without consistent discipline, the architecture’s benefits evaporate within a year.
More files / indirection for small applications.
Easy to over-engineer, not every CRUD app needs ports for every database call.
Risk of anemic domain, domain becomes data containers if rules leak into “service” layers.
Discipline required, without it, the core acquires framework-coupled dependencies and the benefits evaporate.
Onion vs Clean vs Hexagonal#
Hexagonal (Ports & Adapters), two-sided; driving and driven. Emphasizes symmetry.
Onion, concentric layers; domain → application services → domain services → infrastructure. Strict inward-only dependencies.
Clean Architecture, the most prescriptive; Entities → Use Cases → Interface Adapters → Frameworks & Drivers. Adds an explicit “Use Case” layer separate from “Application Services”.
In day-to-day practice they yield similar codebases. Pick the vocabulary the team prefers; don’t bikeshed which name applies.
When to Use#
The conditions where hexagonal pays off. Long-lived products with rich domain logic, multiple driving inputs, and high testing requirements get the most value; throwaway scripts and trivial CRUD see only the costs.
Long-lived products with serious domain logic.
Heavy framework churn, you’ve outlasted two HTTP frameworks already.
Multiple driving inputs, HTTP + queue + CLI + scheduler all invoke the same use cases.
Strong testing requirements, the architecture pays off most in test confidence.
When Not to#
Trivial CRUD, the framework’s defaults are fine.
Throwaway scripts, no point.
Teams without buy-in, the discipline cost outweighs the benefit if half the team writes “just this once” infrastructure calls in the domain.
Common Pitfalls#
The four ways teams adopt hexagonal in name but lose the benefits in practice. An anemic domain, a leaky repository abstraction, framework annotations on domain classes, or layers that multiply faster than the value they add; each one quietly turns the pattern back into a framework-coupled codebase.
Anemic domain, entities with only getters and setters; behavior in service classes. The richer the domain, the more value Hexagonal provides.
Leaky abstractions, a “Repository” that exposes the underlying query language is just an ORM with extra steps.
Framework lock-in via annotations, annotating domain classes with framework attributes pulls the framework into the core.
Too many layers, if a single feature crosses six files, the layering may be over-applied.