Hexagonal Architecture in Practice
also called Ports and Adapters
The application defines ports; adapters implement them — so the same core is driven by HTTP, a queue, or a test with no changes.
Definition
The application core defines ports — interfaces expressed in its own language. Adapters implement them: an HTTP adapter and a message-consumer adapter on the driving side, a database adapter and a payment-provider adapter on the driven side.
The distinguishing insight against plain layering is symmetry. Inputs and outputs are treated the same way; there is no privileged direction. The same core can be driven by a web request, a queue message, a scheduled job or a test harness without knowing which.
What that symmetry enables
- Testing the whole application with in-memory adapters. Not unit tests of classes — full use-case tests with real logic and fake infrastructure, running in milliseconds. This is the property that changes team behaviour most.
- Adding a delivery mechanism cheaply. A service that had an HTTP API gains a queue consumer by writing one adapter.
- The core owns the contract, so a provider's peculiar interface is translated at the boundary rather than shaping the domain — which is the same instinct as an anti-corruption layer.
The rule that keeps it honest
The port is defined in the core's language, not the adapter's. A port called
saveToPostgres(entity) has already failed; the port is orders.save(order) and the adapter knows
about PostgreSQL.
Similarly the port must hide failure semantics appropriately: the core should see a domain-level failure, not a driver exception, or the coupling has simply moved into the error handling.
When it is worth it
The same answer as clean architecture — genuine domain complexity, a long-lived system, multiple delivery mechanisms, or a real intention to replace an infrastructure component. For a small CRUD service it is ceremony.
The strongest single justification in practice is fast, realistic tests. A team that can run its entire business logic suite in two seconds behaves differently from one that needs a database and forty seconds, and that difference compounds over years.
Failure scenarios
- Ports shaped by the adapter, so the abstraction leaks and swapping is still impossible.
- An adapter per class rather than per external concern, multiplying files for nothing.
- In-memory test adapters that diverge from the real ones, so tests pass and production fails. Run a contract test suite against both.
- Applied uniformly where half the services do not need it.
Interview question
"What does hexagonal architecture give you that a conventional layered design does not?"