Hexagonal Architecture

Hexagonal architecture — Alistair Cockburn's original name was ports and adapters, which describes it better — exists to answer one question: how do you write business logic that doesn't know whether it's talking to PostgreSQL or a hash map, to an HTTP request or a CLI argument, to Stripe or a test double?

The mechanism is a strict dependency rule: the domain defines interfaces (ports) describing what it needs, and infrastructure provides implementations (adapters) that satisfy them. Dependencies point inward, always. The domain imports nothing from the outside. A repository interface lives in the domain and its Postgres implementation lives in infrastructure, which means the domain compiles and tests with no database, no web framework, and no network.

The payoff is real and specific: domain logic testable in milliseconds without infrastructure, and infrastructure swappable without touching business rules. The cost is also real — more files, more indirection, and a mapping layer between domain objects and persistence models. For a CRUD application where the domain logic is the database schema, that cost buys very little.

TL;DR

Quick Example

A domain with no idea what infrastructure exists:

Those tests exercise real business rules with no fixtures, no migrations, no container, and no mocking framework. That speed and directness is the argument for the whole pattern.

Core Concepts

The shape

The hexagon shape is incidental — Cockburn chose it to avoid implying a top and bottom, not because six sides mean anything. "Ports and adapters" is the accurate name.

Driving vs. driven ports

Putting the clock and ID generator behind ports is a small detail with a large testing payoff — deterministic tests over time-dependent logic become trivial rather than requiring monkeypatching.

The dependency rule

This is enforceable, and enforcing it mechanically is what keeps it true:

Without automated enforcement, the rule erodes. Someone imports the ORM entity into the domain "just for this one query," and six months later the domain doesn't compile without a database.

Dependency inversion is the whole trick

This is the point people miss when they put repository interfaces in an interfaces/ package alongside the implementations. The interface must live with the domain — it's the domain's statement of what it requires, expressed in the domain's own vocabulary.

The mapping cost

This mapping is the honest cost of the pattern, and it's the reason it's a poor fit for CRUD. If your domain model and your table are the same shape with the same fields, the mapping layer is pure ceremony. If they genuinely differ — because the domain has invariants, value objects, and behavior the schema doesn't express — the mapping is where that difference lives, and it's earning its place.

When It's Worth It

The honest summary: hexagonal architecture is a complexity trade. You add structural complexity to reduce coupling complexity. When there's little coupling complexity to reduce — a CRUD service — you've paid and received nothing.

It relates closely to Clean Architecture (Uncle Bob's concentric circles, with more prescribed layers) and Onion Architecture. All three are the same dependency rule with different diagrams. It also pairs naturally with Domain-Driven Design: DDD tells you what belongs in the domain, hexagonal tells you how to keep infrastructure out of it.

Best Practices

Enforce the dependency rule with tooling

import-linter in Python, ESLint's no-restricted-paths in JavaScript, ArchUnit in Java, or a go vet-style check. A rule maintained by review discipline alone erodes within a quarter. This is the single most important practice here.

Keep the domain genuinely dependency-free

No ORM decorators, no framework base classes, no HTTP status codes, no serialization annotations. If pip installing your domain package would pull in a web framework, it isn't isolated. A useful test: can you run the domain tests with the infrastructure dependencies uninstalled?

Put ports where the domain can see them

Interfaces in the domain package, next to the code that consumes them, named in the domain's vocabulary. AccountRepository, not IAccountDAO. An interface in a shared interfaces/ package next to its implementation hasn't inverted anything.

Write in-memory adapters as first-class code

Not test doubles — real, correct, simple implementations kept alongside the production adapters. They make domain tests fast, they document the port's contract, and they're useful for local development without infrastructure.

Test the port contract once per adapter

A shared test suite run against both the in-memory and the Postgres adapter, asserting they behave identically. This catches the case where your fast tests pass against an in-memory implementation that doesn't match real database semantics — the main risk of the pattern.

Translate errors at the boundary

Domain exceptions (InsufficientFunds) map to HTTP statuses in the HTTP adapter, to exit codes in the CLI adapter, to retry decisions in the queue adapter. The domain never knows what a 422 is.

Put the clock and ID generation behind ports

Clock.now() and IdGenerator.next() as injected ports make time-dependent and ID-dependent logic deterministic in tests without patching globals. It's a small amount of ceremony for a disproportionate testing improvement.

Apply it selectively

One bounded context with genuine complexity can be hexagonal while the CRUD admin service beside it isn't. Architectural consistency is worth less than each service having the structure its problem warrants.

Common Mistakes

The domain importing infrastructure

Interfaces on the wrong side

Anemic domain with logic in the services

This is the most common way the pattern is adopted in name only. If all the business logic is in service classes and the entities are dataclasses, you have layers, not a domain.

Ports that leak infrastructure

One adapter per port, forever

Applying it to a CRUD service

FAQ

How is this different from Clean Architecture?

They're the same idea with different presentation. Cockburn's ports and adapters (2005) states the dependency rule and the port/adapter vocabulary. Clean Architecture (Martin, 2012) adds prescribed concentric layers — entities, use cases, interface adapters, infrastructure — and more naming conventions. Onion Architecture is a third variant. Choose whichever diagram helps your team; the dependency rule is the substance in all of them.

Doesn't this add a lot of boilerplate?

Yes, and that's the trade. You get more files, an interface per external dependency, and a mapping layer between domain and persistence models. The return is fast isolated tests and swappable infrastructure. For a domain with real invariants and a multi-year life, it pays. For CRUD, it doesn't, and applying it there is the most common way teams conclude the pattern is bad.

Do I need a DI container?

No. Constructor injection wired up manually in a composition root — one function that builds the object graph at startup — is sufficient and clearer for most applications. A container becomes useful with many dependencies, scoped lifetimes, or a framework that expects one. Explicit wiring has the advantage that the object graph is readable code rather than annotation magic.

Can I use my ORM entities as domain models?

You can, and it's a genuine tradeoff rather than simply wrong. Sharing them removes the mapping layer and couples your domain to the ORM — lazy loading, session lifecycle, and ORM-imposed constructor requirements all leak into domain code, and the domain no longer tests without the ORM. Many pragmatic teams accept this. If you do, be honest that the domain isn't independent, and don't claim the testing benefit you haven't bought.

How does this work with CQRS?

Well, and they compose naturally. Commands go through the domain and its ports, enforcing invariants. Queries frequently bypass the domain entirely and read from a projection or a direct SQL query — because reads don't need invariant enforcement, and forcing them through the domain model produces N+1 problems and awkward mapping for no benefit. See CQRS & Event Sourcing.

Is it worth it for microservices?

Depends entirely on the service's complexity, not on the architectural style around it. A microservice that owns real business rules benefits. One that's a thin API over a table — which describes many services — doesn't. Applying it uniformly across every service because it's "the architecture" is how the pattern gets a reputation for over-engineering. See Microservices.

Related Topics

References