pattern

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.

hexagonalports-adapterstestingboundariesdependency-inversion

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?"