The idea: the application in the middle, technology at the edges
An application has a job (here: take an order, charge for it, store it, tell the customer) and a lot of technology around that job: HTTP, a command line, tests, a database, a payment provider, a mail server. Hexagonal architecture, also called ports and adapters (Alistair Cockburn, 2005), keeps the two apart. The application core sits inside the hexagon. It talks to the outside only through ports. Outside the hexagon, adapters connect one technology to one port.
The six sides mean nothing. Cockburn picked a hexagon so there is room to draw several ports, and so that nobody draws it as layers stacked from top to bottom.
The canvas follows one order, BOOK × 2, through every hop: actor → driving adapter → driving port → use case → domain → three driven ports → their adapters → PostgreSQL, Stripe and SMTP, and the answer back. Blue boxes are technology messages outside the hexagon, purple boxes are calls through a port, green boxes are domain objects and red boxes are errors. Demo 1: happy path (REST).
Ports: interfaces the core owns
A port is an interface, written in the core's words, owned by the core. There are two kinds:
| Driving (primary) port | Driven (secondary) port | |
|---|---|---|
| On the canvas | left: PlaceOrderUseCase | right: OrderRepository, PaymentPort, NotificationPort |
| Who calls | an adapter calls the core | the core calls an adapter |
| Who implements it | the core (PlaceOrderService) | an adapter (PostgresOrderRepository, …) |
| Typical names | use cases, application services, commands | repositories, gateways, notifiers, clocks |
Ports describe what the application needs, not how it is done: NotificationPort.orderPlaced(order) says "tell the customer", not "send an e-mail over SMTP".
Adapters translate, both ways
An adapter is a translator between one technology and one port. Every adapter on the canvas shows its translation in orange under its box.
| Adapter | In | Out |
|---|---|---|
OrderController (REST) | JSON body → PlaceOrderCommand | OrderResult → 201 Created; InvalidQuantity → 422; declined → 402; unavailable → 503 |
OrderCli | arguments → PlaceOrderCommand | a line of text and an exit code (0, 2, 3, 75) |
PostgresOrderRepository | Order → INSERT row (total in cents) | SQLException → RepositoryUnavailable |
StripePaymentAdapter | Money(24.00 USD) → amount=2400¤cy=usd | 402 card_declined → PaymentResult.declined |
SmtpNotifier | Order → a MIME e-mail | 250 OK → returns |
The rule: HTTP status codes, JSON, SQL and Stripe's error codes never enter the hexagon, and domain objects like Order never leave it. Each side keeps its own vocabulary.
Dependencies point inward, calls go both ways
Press Show Dependencies (Demo 7: dependencies). A purple arrow means "this class names that one in its source code". Every arrow ends on a port, inside the hexagon. On the left this is natural: the controller calls the use case. On the right it is not: at run time the core calls the Postgres adapter, but in the source code the adapter depends on the core, because it implements OrderRepository. That is the dependency inversion principle: the call goes out, the dependency points in.
So the core compiles without any adapter, any database driver or any web framework on the classpath.
The composition root: the only place that names adapters
Something has to choose the adapters and plug them in. That is Main, the composition root (in Spring: the configuration and component scan; in other code: a main() method). It is the only code that names both the core and the concrete adapters. Change an adapter with the selects and watch: one line of Main turns orange, and the status under the hexagon says the core has 0 lines changed. Demo 5: same use case via CLI runs one order over REST, swaps the driving adapter, and runs the next order from the terminal through the same port.
Where failures belong
- Business rule:
qty 9breaks "1 ≤ qty ≤ 5".Order.createthrows inside the core, before any driven port is called: no charge, no row, no mail. The REST adapter turns it into422, the CLI into exit code 2. Demo 2. - A "no" from the outside: Stripe declines the card. The adapter turns
402 card_declinedintoPaymentResult.declined, and the core decides what that means: the order is not saved. Demo 3. - Infrastructure failure: PostgreSQL is down. The adapter catches
SQLExceptionand throwsRepositoryUnavailable, an exception that belongs to the port. The core sees a failure in its own words and makes a business decision: the customer was charged, so refund throughPaymentPort.refund. Demo 4 takes 607 ms (two Stripe calls) and ends with503.
Testing through the ports
Cockburn's original goal was to let an application "be equally driven by users, programs, automated tests or batch scripts, and be developed and tested in isolation from its eventual run-time devices and databases". Demo 6: test with fakes plugs a test driver into the driving port and in-memory fakes into the three driven ports. The same PlaceOrderService runs in 1 ms instead of 387 ms, nothing leaves the process, and the test can look inside the fakes (the payment was recorded, one notification was printed).
A fake is a small working implementation of the port (a HashMap repository). A mock records calls so the test can check them. Either works, because the port is a plain interface. The real adapters still need their own integration tests against a real PostgreSQL or Stripe's test mode, but there are few of them and they test only the translation.
Hexagonal, layered, onion, clean
| Style | Picture | Database dependency |
|---|---|---|
| Classic layered (3-tier) | presentation → business → data access, top to bottom | business layer depends on the data-access layer: points outward |
| Hexagonal (Cockburn 2005) | core inside, ports on the edge, adapters outside; left = driving, right = driven | inverted: the adapter depends on the core's port |
| Onion (Palermo 2008) | domain model in the centre, domain services, application services, infrastructure in the outer ring | inverted, same as hexagonal |
| Clean architecture (Martin 2012) | entities, use cases, interface adapters, frameworks; "the dependency rule": source dependencies point inward | inverted, same as hexagonal |
Onion and clean architecture add rings inside the core (entities vs use cases). Hexagonal says nothing about the inside; it is only about the boundary. On the canvas the inside has two parts anyway: the use case PlaceOrderService and the domain Order.
The leaky core
Demo 8: leaky core shows the anti-pattern. PlaceOrderService calls DriverManager.getConnection and runs the INSERT itself. The OrderRepository port and its adapter are bypassed, and a red arrow goes from the core straight to PostgreSQL: a dependency pointing outward. Swap the repository to in-memory: there is nothing to plug the new adapter into, so the business code has to be edited (the status turns red). With the database down, a java.sql.SQLException now lands in the business logic. A test of the core needs a running PostgreSQL.
It usually starts small: one "quick" query in a service, an HTTP request object passed into the domain, a JPA annotation on a domain entity. Each one is a dependency from the inside to the outside.
What it costs
More types: a port per external need, a command and a result per use case, and a mapping in every adapter (Order ↔ row, Money ↔ cents). For a small CRUD service that only moves rows between HTTP and a table, that mapping can be most of the code, and a plain layered design is often enough. The pattern pays off when the domain rules are worth protecting, when there are several ways in (API, CLI, messages, scheduled jobs), or when the infrastructure is expected to change.
What the page leaves out
More than one use case per port, transactions and the unit of work (save and charge are not atomic here; the refund is a hand-written compensation, see Saga vs Two-Phase Commit), the transactional outbox for notifications, domain events, message-driven adapters (a Kafka consumer is just another driving adapter), separate read models (CQRS), package and module rules that enforce the dependency direction (Java modules, ArchUnit), and the Spring wiring itself.