Hexagonal, Clean, Onion: dependencies point inward
There is a pattern that has been rediscovered under three different names and illustrated with three different shapes. Alistair Cockburn called it hexagonal architecture in 2005 and drew it as a hexagon. Jeffrey Palermo called it onion architecture in 2008 and drew it as concentric rings. Robert Martin called it clean architecture in 2012 and drew it as concentric rings with the arrows labeled. The shapes are different. The rule is identical: dependencies point inward, toward the domain, never outward.
That is the whole pattern. Everything else — ports, adapters, inversion of control, infrastructure-free tests — is machinery in service of that single constraint.
It is worth being specific about what the constraint excludes, because the default layout in most frameworks violates it. In a standard web-app layout, the domain logic lives in service classes that depend on repository classes that depend on ORM classes that depend on the database. Arrows point outward: the domain knows about persistence, persistence knows about the specific database. A change to the database propagates inward. A change to the framework propagates inward. The domain, which should be the most stable part of the system, is at the bottom of the dependency tree and therefore the most exposed to churn from above.
The inverted layout puts the domain at the center. The domain depends on nothing — not the database, not the web framework, not the message broker. Everything else depends on the domain. Infrastructure adapts to the domain’s interfaces, not the other way around. This is the only real content of hexagonal, onion, and clean architecture. The diagrams differ; the rule does not.
Ports and adapters
Cockburn’s vocabulary is the clearest. The domain exposes ports — interfaces that describe what the domain needs from the outside world or offers to it. Adapters are the implementations that plug into those ports on the infrastructure side.
Two kinds of ports:
- Driving ports (or inbound) are how the outside world invokes the domain. An HTTP controller, a CLI command, a message consumer, a scheduled job — all of these are adapters that call into a driving port. The port is an interface owned by the domain; the adapter is owned by the delivery mechanism. The domain does not know whether it is being invoked by a web request, a test, or a job.
- Driven ports (or outbound) are how the domain invokes the
outside world. A repository interface, a notification sender, a
payment gateway, a clock. The domain defines the interface in terms
of what it needs —
OrderRepository.findById, notPostgresOrderTable.selectById. The adapter provides the concrete implementation.
The crucial property is that the interface is defined in the domain’s vocabulary, on the domain’s side of the boundary, in service of the domain’s needs. An adapter implements an interface it did not define. This is dependency inversion: the high-level policy (the domain) defines the abstraction, and the low-level detail (the adapter) depends on the abstraction. Without this inversion, “we have a repository interface” is just a layer of indirection that still lets the database’s vocabulary dictate the domain’s shape.
In practice, this means an OrderRepository interface lives beside the
Order aggregate, in the same package, owned by the same team that owns
the domain. A PostgresOrderRepository implementation lives in a
separate package that imports the domain package. If the import goes
the other way — the domain imports from persistence — the architecture
is lying about itself.
Why the shape does not matter
Hexagonal, onion, and clean architecture look different because they emphasize different things.
- Hexagonal emphasizes the symmetry of the sides. Any side of the hexagon is a port; driving and driven ports are peers. The hexagon is a neutral shape — six sides for no reason other than “not four” — chosen specifically to avoid implying a top/bottom or front/back.
- Onion emphasizes the concentric layering. Domain at the center, domain services around it, application services around those, infrastructure at the edge. Each ring can depend inward but never outward.
- Clean emphasizes the dependency rule explicitly and adds vocabulary — entities, use cases, interface adapters, frameworks and drivers — for each ring, along with the observation that the rule forces a specific organization of the code.
Squint at the three diagrams and they are the same architecture drawn three ways. The port is the inner edge of the outermost ring; the adapter is the outermost ring itself. The hexagon’s sides are slices through the onion. The shapes are pedagogical choices, not design differences.
If there is a real distinction, it is that hexagonal talks most directly about testability (the domain is driven from both sides, and either side can be substituted with a test double), while clean and onion talk most directly about stability (the stable things depend on nothing, the unstable things depend on the stable). These are two views of the same property.
The test that the architecture is real
There is a single question that separates teams who have adopted this architecture from teams who have adopted its folder structure: can the domain code be compiled, run, and tested without the database, the web framework, the message broker, or the HTTP client?
If yes, the architecture is real. The domain is independent; the dependencies point inward; the adapters are genuinely substitutable. Tests of the domain run in milliseconds, with no fixtures, no containers, no mocked ORMs, no database migrations. Entire aggregate behaviors are tested as plain objects.
If no — if the domain cannot be exercised without spinning up Postgres,
or without the Spring context loading, or without a Kafka container —
then the dependency arrows are pointing the wrong way somewhere. The
folder called domain imports something from the folder called
infrastructure, or it inherits from a framework base class, or it is
annotated into existence by a library. The shape on the whiteboard
says hexagonal; the compiler says otherwise.
This is a mechanical test, and it is worth running on any codebase that claims to be hexagonal. The compiler is not fooled by folder names.
Where the logic actually lives
An architecture that gets the dependency direction right still has to answer: where does the business logic go? Hexagonal/clean/onion is silent on this by itself. The common failure is to put the domain in the right place and then hollow it out.
The failure mode has a name: the anemic domain model. Domain objects
become bags of data with getters and setters. All the behavior lives in
“service” classes — OrderService, PaymentService,
ShipmentService — that manipulate the bags. The architecture diagram
looks correct, the dependency arrows point inward, and the domain is
dead. Every service knows the internals of every entity. Invariants are
enforced (when they are enforced) by scattered checks in the service
layer. The fact that the domain is framework-free does not save it;
anemia is orthogonal to dependency direction.
The cure is to put behavior back onto the domain objects. An Order
knows how to be placed, cancelled, or fulfilled. A Money knows how to
add itself to another Money (and knows to refuse if the currencies
differ). A Shipment knows its own lifecycle. Application services
coordinate across aggregates — fetching, dispatching, persisting — but
the rules live on the objects themselves. This is the DDD view, and
it is what turns hexagonal architecture from a folder layout into a
working model.
The guideline: if you can replace every class in your “domain” folder with a struct and lose nothing but syntax, the domain is anemic. If the classes enforce invariants, reject illegal states, and express domain verbs — the domain is real.
The adapter’s job
Adapters are where the outside world’s ugliness is allowed to live. A
Postgres adapter deals with SELECTs and transactions and connection
pools. An HTTP controller deals with request parsing, serialization,
HTTP status codes. A Kafka consumer deals with partition assignment,
offset commits, deserialization.
None of that leaks inward. The adapter translates between the outside world’s vocabulary and the domain’s vocabulary. A repository adapter reads rows, constructs aggregates, and hands them over. An HTTP controller parses a request, constructs a command, dispatches it, and serializes the response. The domain receives and returns domain types. It does not know what format they arrived in.
This is where the architecture pays rent. Swapping Postgres for something else is an adapter rewrite, not a domain rewrite. Adding a gRPC API alongside the REST API is a second inbound adapter, not a refactor. Migrating from RabbitMQ to Kafka is a consumer adapter rewrite. Replacing the HTTP client library for an outbound integration is confined to a single file. None of this is free — the adapter has to be written — but none of it forces the domain to change.
The corollary is that adapters are also where most of the code is. Mapping code, error translation, retries, serialization — this is real work, and the architecture does not eliminate it. It localizes it. An adapter is allowed to be ugly. The domain is not.
Where it pays and where it does not
Hexagonal/clean/onion is not free. Every port is an extra interface. Every adapter is an extra file. A small application with one database and one HTTP surface, where persistence is never going to change and the domain is mostly CRUD, pays the architectural tax without getting anything in return. The right shape for that application is a straightforward layered app — controller, service, ORM — and forcing ports and adapters onto it produces ceremony without benefit.
The architecture pays when one or more of the following is true:
- The domain is genuinely complex. The value of a framework-free domain is being able to reason about it, test it, and evolve it without fighting infrastructure. A CRUD app has no domain to protect.
- The infrastructure is expected to change. Swapping databases, adding a second delivery mechanism, migrating from one queue to another — the architecture earns its keep the first time any of these happen.
- Testing speed and isolation matter. Tests that exercise the domain as plain objects run three orders of magnitude faster than tests that boot the framework and a database. On a team that tests thoroughly, this is the difference between a ten-second feedback loop and a ten-minute one.
- Multiple teams contribute. The port/adapter boundary is a clean place to split ownership. One team owns the domain; another owns a specific adapter. The interface is the contract.
Absent those conditions, simpler is better. A clean-architecture skeleton imposed on a CRUD app is one of the most common forms of over-engineering in modern codebases.
The relationship to DDD and microservices
Hexagonal/clean/onion is the tactical shape that DDD’s domain model wants to live inside. DDD says: model the business in a rich domain of aggregates, value objects, and events. Hexagonal says: keep that model free of infrastructure so it can evolve at the pace of the business rather than the pace of the framework. The two patterns compose naturally — so naturally that most modern DDD examples are hexagonal without naming it — and the combination is what makes a bounded context something you can actually implement and change.
The relationship to microservices is more subtle. A well-designed microservice is hexagonal inside: a domain at its core, ports for its inputs and outputs, adapters at the edges. A microservice built as a thin wrapper around a database — routes going straight to SQL — is a distributed CRUD system with network calls in the middle, and it will have all of microservices’ operational cost and none of the autonomy. The dependency-direction rule is what keeps each service’s core stable enough to justify its independent lifecycle.
The rule, once more
Dependencies point inward. The domain does not know about the database. The domain does not know about HTTP. The domain does not inherit from a framework class. The domain defines the interfaces it needs; infrastructure implements them. Tests of the domain do not touch the network or the disk.
That rule, consistently applied, is what hexagonal architecture is. The hexagon is decoration. The rings are decoration. The names are decoration. The direction of the arrows is the architecture. Get the arrows right and almost every other tactical decision — how to test, where to put the logic, how to handle change — has an obvious answer. Get them wrong and no amount of folder structure will save the design.
That is the whole job.