Hexagonal architecture (Alistair Cockburn, 2005) — also called Ports & Adapters and, in Robert Martin's variant, Clean Architecture — puts your business domain in the center and pushes everything else to the edges. The domain depends on nothing concrete. HTTP, databases, message brokers, file systems are all adapters that plug into ports the domain owns. Swap any one of them without touching business logic.
← Back to Architecture@Entity, no SQL.Postgres → DynamoDB? Swap one adapter. REST → gRPC? Add a new driving adapter, leave the old one. Stripe → Adyen? New payment adapter. The use cases don't change because they never knew which one was wired in.
Unit tests for use cases inject fake adapters — in-memory repository, fake email sender. No DB container, no network, no flaky integration. Domain logic tests run in milliseconds.
Frameworks come and go (Struts → Spring MVC → WebFlux → who's next). When the domain is framework-free, you survive each migration with a stable core and a rewritten outer ring.
Without an ORM in the room, you can't blur "domain" with "row in the orders table." Entities have to express invariants in code; the persistence shape is a separate problem solved by an adapter.
Typical packages:
domain/ — entities, value objects, domain services. No third-party imports.application/ — use cases (one class per business operation), driving and driven port interfaces.adapters/in/ — HTTP controllers, message consumers, CLI commands.adapters/out/ — DB repositories, external API clients, email senders.config/ — composition root: wires concrete adapters to ports.Driving (inbound): the use-case interface. PlaceOrderUseCase.execute(command). The HTTP controller calls this; so could a Kafka consumer or a CLI command. The domain doesn't know which.
Driven (outbound): what the use case needs from the world. OrderRepository.save(order), PaymentGateway.charge(amount). The use case calls the interface; an adapter implements it.
No @Entity, no @JsonProperty, no DB session, no HTTP request object. If your domain class needs a JPA annotation, you've leaked persistence into the domain. Use a separate persistence model in the adapter; map between the two at the boundary.
One place — startup, the DI container's config — wires concrete adapters to port interfaces. PlaceOrderUseCase(new PostgresOrderRepository(), new StripePaymentGateway(), ...). Everything else only sees interfaces.
Adapters translate between the outside world's shape and the domain's. HTTP request → command DTO → use case input. Domain output → response DTO → JSON. JPA entity → domain entity in the repository adapter. This mapping is the price; it's what keeps the domain pure.
Adding a field can mean: domain entity, JPA entity, mapper, request DTO, response DTO, port interface, adapter implementation. For a CRUD app where the domain has no real logic, this overhead doesn't pay back. Hexagonal is for rich domains.
The domain entity and the persistence entity are usually distinct classes. Engineers new to the codebase often try to "simplify" by merging them — and quietly destroy the boundary. Documentation and code review have to defend it.
Not every service deserves hexagonal. A glue service that calls one third-party API and writes to one queue doesn't need a domain layer; it is an adapter. Forcing the structure adds friction with no benefit.
| Name | Author | Twist |
|---|---|---|
| Hexagonal / Ports & Adapters | Cockburn (2005) | The original. Hexagon is metaphor only — could have any number of sides. |
| Onion Architecture | Jeffrey Palermo (2008) | Concentric rings; emphasizes domain-centric layering. |
| Clean Architecture | Robert C. Martin (2012) | Adds the explicit Use Case layer between Entities and Adapters. |
All three share the dependency rule: source code dependencies point inward. Pick whichever vocabulary your team already knows; the substance is the same.
Worth the boilerplate when:
Skip it for thin CRUD apps, glue services, prototypes, and anything where the domain logic is essentially "save this and notify that."