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).

The hexagon holds the application core: PlaceOrderService and the Order domain object. On its left edge the driving port PlaceOrderUseCase is used by the OrderController (REST) and OrderCli adapters outside. On its right edge the driven ports OrderRepository, PaymentPort and NotificationPort are used by the service and implemented by PostgresOrderRepository, StripePaymentAdapter and SmtpNotifier, which talk to PostgreSQL, Stripe and SMTP
Ports sit on the edge of the hexagon and belong to the core; adapters outside plug one technology into one port.

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) portDriven (secondary) port
On the canvasleft: PlaceOrderUseCaseright: OrderRepository, PaymentPort, NotificationPort
Who callsan adapter calls the corethe core calls an adapter
Who implements itthe core (PlaceOrderService)an adapter (PostgresOrderRepository, …)
Typical namesuse cases, application services, commandsrepositories, 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.

AdapterInOut
OrderController (REST)JSON body → PlaceOrderCommandOrderResult → 201 Created; InvalidQuantity → 422; declined → 402; unavailable → 503
OrderCliarguments → PlaceOrderCommanda line of text and an exit code (0, 2, 3, 75)
PostgresOrderRepositoryOrder → INSERT row (total in cents)SQLException → RepositoryUnavailable
StripePaymentAdapterMoney(24.00 USD) → amount=2400&currency=usd402 card_declined → PaymentResult.declined
SmtpNotifierOrder → a MIME e-mail250 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.

Inside the hexagon PlaceOrderService uses the OrderRepository interface. At run time an orange arrow shows the save(order) call going out to PostgresOrderRepository and on to the database; a purple arrow shows the source dependency from PostgresOrderRepository pointing back in to the interface it implements
The call goes out to the adapter, but the adapter's source code depends on the core's port: the dependency points in.

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 9 breaks "1 ≤ qty ≤ 5". Order.create throws inside the core, before any driven port is called: no charge, no row, no mail. The REST adapter turns it into 422, the CLI into exit code 2. Demo 2.
  • A "no" from the outside: Stripe declines the card. The adapter turns 402 card_declined into PaymentResult.declined, and the core decides what that means: the order is not saved. Demo 3.
  • Infrastructure failure: PostgreSQL is down. The adapter catches SQLException and throws RepositoryUnavailable, 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 through PaymentPort.refund. Demo 4 takes 607 ms (two Stripe calls) and ends with 503.

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

StylePictureDatabase dependency
Classic layered (3-tier)presentation → business → data access, top to bottombusiness layer depends on the data-access layer: points outward
Hexagonal (Cockburn 2005)core inside, ports on the edge, adapters outside; left = driving, right = driveninverted: the adapter depends on the core's port
Onion (Palermo 2008)domain model in the centre, domain services, application services, infrastructure in the outer ringinverted, same as hexagonal
Clean architecture (Martin 2012)entities, use cases, interface adapters, frameworks; "the dependency rule": source dependencies point inwardinverted, 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.