Hexagonal Architecture
Also called ports and adapters. Hexagonal architecture isolates your core business logic from external concerns (databases, APIs, UIs), making systems testable and adaptable.
The Concept Explained
Most applications are written with the business logic depending on the infrastructure. The order service imports the database library, constructs SQL, calls the payment provider's SDK, and returns a response shaped by the web framework. The rules of the business are woven through the mechanics of the tools.
Hexagonal architecture, proposed by Alistair Cockburn, inverts that. The business logic sits in the centre and depends on nothing external. Everything outside, databases, web frameworks, message brokers, third-party APIs, depends inward on the core.
The mechanism is interfaces, called ports, defined by the core in its own terms. The core declares "I need to be able to save an order" as an interface it owns. Infrastructure supplies adapters implementing those interfaces: a Postgres adapter, an in-memory adapter for tests, a different adapter if the database changes.
The whole pattern is one rule about dependency direction: dependencies point inward, toward the domain, never outward. The core defines what it needs in its own vocabulary, and the outside world conforms. The hexagon shape carries no meaning beyond suggesting several sides for several adapters; the arrows are the pattern.
How It Works
Ports come in two kinds and distinguishing them clarifies the picture considerably.
Driving ports, on the input side, are how the outside world invokes the core. An HTTP handler, a message consumer, a CLI command, and a scheduled job are all adapters that call the same driving port. The core does not know which one invoked it, which is why the same logic serves an API, a queue consumer, and a test with no changes.
Driven ports, on the output side, are what the core needs from the outside. Persistence, sending an email, calling a payment provider. The core declares the interface and infrastructure implements it.
Conventional Layering vs Ports and Adapters
Conventional layering
Business logic depends on infrastructure
Ports and adapters
Infrastructure depends on the core
Why Testability Improves So Much
This is the benefit that actually justifies the pattern in practice, and it is worth being concrete about.
When business logic depends on a database, testing it requires a database: a container to start, a schema to migrate, fixtures to load, state to reset between tests. Those tests are slow, and slow tests get run less often, and rules that are expensive to verify accumulate bugs.
When the logic depends on an interface it defined, tests supply an in-memory implementation. The tests become pure computation: construct the core with a fake repository, invoke it, assert on the result. Thousands run in seconds, so they run on every save.
The second-order effect matters more than the speed. When testing a business rule is trivial, rules get tested thoroughly. When it requires infrastructure, coverage quietly concentrates on the easy paths.
The most common failure is defining ports in the vocabulary of the infrastructure rather than the domain. A port called OrderRepository with methods like executeQuery or findByCriteria has inverted the dependency on paper while leaking the database's model into the core. The port should say what the domain needs, findOrdersAwaitingShipment, not how storage works. If swapping the adapter would require changing the port, the port is wrong.
What It Costs
Indirection. Every external interaction passes through an interface, so following a request through the code means jumping between the port and its implementation. For simple operations this is genuine overhead in comprehension.
More types. A port, at least one real adapter, a test adapter, and often a translation between the domain model and the persistence model. That last one is real work people underestimate.
Wiring. Something must construct the core with the right adapters. Dependency injection, whether by framework or by hand, becomes necessary.
Resistance to convenience. Frameworks offer shortcuts that reach directly from a controller to a database. The pattern forbids them, and that discipline has a cost in typing.
For a small application with straightforward CRUD, that cost buys little. The abstraction pays off in proportion to how much genuine business logic exists.
System Design Implications
Apply it where business rules are substantial and long-lived. Order pricing with promotions and tax rules, insurance underwriting, financial calculations, anything where the logic is intricate and outlives several generations of infrastructure. A CRUD service that stores and returns records has almost no core to protect.
The portability benefit is real but usually secondary. Teams rarely swap databases. The everyday value is testability and the clarity that comes from a domain expressed in its own vocabulary. Leading with "you could change database" oversells the pattern; leading with test speed and rule coverage is honest.
It composes with the rest of this course. In microservices, each service can be internally hexagonal. In serverless, keeping logic in plain functions behind ports and confining platform specifics to a thin adapter is precisely how you limit lock-in, which is the mitigation named in the serverless lesson. In event-driven systems, a message consumer is just another driving adapter over the same core.
Apply it selectively. Not every service needs it, and a codebase where some services are hexagonal and others are simple layered CRUD is a reasonable outcome rather than an inconsistency. The decision should track where the business logic actually is.
Watch the model translation boundary. Keeping the domain model separate from the persistence model is what preserves the purity, and it is also the largest ongoing cost. Some teams share the model and accept partial leakage, which is a legitimate pragmatic choice as long as it is a choice.
The strongest answer names the dependency direction first and the benefit second. Dependencies point inward, so the core defines interfaces in domain terms and infrastructure implements them. The payoff is that business rules can be tested with in-memory adapters in milliseconds instead of requiring a database, so they get tested thoroughly. The cost is indirection and extra types, which is why it earns its place where real business logic exists and not in CRUD.
Tradeoffs and Decision Framework
| Dimension | Conventional layering | Hexagonal |
|---|---|---|
| Business logic tests | Need infrastructure | Pure, in-memory |
| Test runtime | Seconds to minutes | Milliseconds |
| Swapping infrastructure | Touches business code | New adapter only |
| Types and files | Fewer | More |
| Tracing a request in code | Direct | Through indirection |
| Framework coupling | Throughout | Confined to adapters |
| Suits | CRUD, thin services | Rich domain logic |
The framework in three questions. How much genuine business logic exists, since the pattern protects a core and a service without one has nothing to protect? How long will this system live, because the payoff accrues over infrastructure and framework changes? And is test speed currently limiting how thoroughly rules are verified, which is the most concrete symptom the pattern addresses?
If the answer is a thin service over a database with a long-term horizon of a couple of years, conventional layering is fine and simpler. If it is a system encoding rules the business cares about, expected to outlive its current stack, the discipline pays for itself.
Common Mistakes
Defining ports in infrastructure vocabulary. A port exposing query builders or persistence concepts has inverted the dependency on paper only.
Applying it to trivial CRUD. Indirection with no core to protect is cost without benefit.
Letting framework types into the core. An HTTP request object or an ORM entity in the domain reintroduces the coupling the pattern removes.
Skipping the model translation. Sharing one model between domain and persistence is a pragmatic choice, and treating it as free means the persistence schema quietly starts driving the domain.
Treating the hexagon as meaningful. Six sides carry no significance. Some readings of the diagram get lost looking for meaning that is not there.
Only writing production adapters. The in-memory test adapter is where most of the value is realized, and skipping it means the tests still need infrastructure.
Assuming it is the same as microservices. It governs internal structure within one deployable unit. The two are orthogonal and compose well.
Explain hexagonal architecture. How does isolating business logic from infrastructure improve a system, and what does it cost in complexity?