Leer en español · Lire en français · Auf Deutsch lesen
Section 5.6 of 5.8 · Intermediate · 25 min read
Hexagonal, onion and clean architecture
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
This lesson covers one idea with three names: hexagonal, onion and clean architecture. It keeps your business rules independent of databases, frameworks and external services, so you can test them quickly and replace the technology around them.
Think of a travel adapter. Your laptop charger has one plug shape, and each country has its own socket. You don't rebuild the laptop for every country. You carry small adapters that fit the socket on one side and your plug on the other. These styles build software the same way. The business rules have their own standard sockets, called ports. Databases, websites, payment companies and test tools connect through adapters, so any of them can be swapped without touching the rules.
These styles grew out of frustration with layering, where the business layer depends on persistence, so rules end up shaped by tables, and testing a rule means starting a database. Alistair Cockburn's answer, in 2005, was hexagonal architecture, also called ports and adapters. Jeffrey Palermo's onion architecture followed in 2008. And Robert Martin brought these ideas together as clean architecture, in a blog post in 2012 and a book in 2017. All three flip the dependency. The business code declares the interfaces it needs, and the infrastructure implements them.
Here's the hexagon. Inside sits the application: the domain and its use cases. The shape itself has no meaning; what matters is inside versus outside. A port is an interface the application owns. On the left are driving ports, such as Place Order and Track Order: the application's own API. Driving adapters call them: a REST controller, a mobile app's API, a test harness or a batch script. Each turns its input into a call with domain values. On the right are driven ports, such as Orders and Payments. The application calls them when it needs something from the outside world. Driven adapters implement them: a PostgreSQL repository or an in-memory one, a real card provider or a fake. At run time the application calls the adapter, but in the code, the adapter depends on the application.
Clean architecture draws the same idea as rings. At the centre are entities: enterprise-wide rules, such as Order and Money. Around them, use cases: application-specific rules, such as Place Order. Then interface adapters: controllers, presenters and repository implementations. And at the edge, frameworks and drivers: the web framework, the database, the user interface. The dependency rule ties it together: source code dependencies point only inwards. Nothing in an inner ring may name anything declared in an outer one.
Here's why it matters. The food-delivery platform must move to a new card provider within six weeks, because of a pricing change. In one version of the system, checkout calls the old provider's software development kit directly, in twenty three places. Its error types are caught in eleven places, and its objects are stored in the database. Every call site must change, the stored data must be migrated, and the payment tests rewritten. That's five to six weeks, with a risky all-at-once switch. In the other version, one adapter class of about two hundred lines implements a payment gateway port. The core knows only charge, refund and its own result type. So the team writes a second adapter, runs the port's existing tests against it, then moves one per cent of payments, then ten, then all. Two weeks, reversible at every step. The port cost about three extra days at the start. Its value depends on how likely and costly the change is. And it must speak the domain's language, not mirror one vendor's API.
Use this structure when the business logic is substantial and will outlive the current frameworks, and when fast, isolated tests of business rules matter. It also pays off when you integrate with external services that are likely to change. Be cautious with prototypes, short-lived tools and applications that mostly move form fields into tables. There, extra interfaces and mapping code add files without adding protection. And as with layers, enforce the rule automatically, with import-linter, with ArchUnit's ready-made onion architecture rule, or with a domain module that won't even compile with a framework dependency.
The full section builds the Place Order use case in Python, with a real adapter and a test double, and compares the three styles side by side. Next, in Section five point seven, we find good boundaries with domain-driven design. See you there.
What you need to know
- Hexagonal (ports and adapters, Cockburn, 2005), onion (Palermo, 2008) and clean architecture (Martin, 2012 blog, 2017 book) share one idea: business logic at the centre, independent of frameworks and infrastructure, with dependencies pointing inwards.
- A port is an interface owned by the application; an adapter translates between a port and a technology. Driving adapters call the application; driven adapters implement interfaces the application calls.
- The dependency rule says inner code must never name anything in an outer ring; dependency inversion lets control flow outwards while source code dependencies point inwards.
- The main payoffs are fast, isolated tests of business rules and the ability to replace infrastructure such as payment providers by writing a new adapter.
- The costs are extra interfaces, mapping code and indirection, which are not worth paying for simple CRUD applications or short-lived tools.
- Enforce the rule automatically, for example with import-linter, ArchUnit's onionArchitecture rule or separate build modules.
One idea, three names
Hexagonal architecture, onion architecture and clean architecture are three formulations of one idea: put the business logic at the centre, make it independent of user interfaces, databases, frameworks and external services, and make all source code dependencies point towards it. They describe the internal structure of an application, so they combine with any deployment style: a single monolith, a modular monolith whose modules are each built this way, or a microservice.
They grew out of a frustration with the classic layered architecture. In the layered style of Section 5.2 the business layer depends on the persistence layer, which depends on the database, so business rules end up shaped by tables and object-relational mapping, and testing a rule means starting a database. The family of styles in this section flips that dependency with the dependency inversion principle: the business code declares the interfaces it needs, and the infrastructure code implements them. The principle itself is covered in Section 3.5; this section shows the architectures built on it.
Hexagonal architecture: ports and adapters
Alistair Cockburn first described the idea on the Portland Pattern Repository wiki and wrote it up as an article in 2005 under the more descriptive name ports and adapters. His stated intent was to allow an application to equally be driven by users, programs, automated test or batch scripts
, and to develop and test it in isolation from its eventual databases and devices. In 2024 he and Juan Manuel Garrido de Paz published a short book, Hexagonal Architecture Explained, restating the pattern.
A port is a purposeful conversation between the application and the outside world, expressed as an interface: "place an order", "store orders", "charge a card". An adapter is the code that translates between a port and a particular technology: an HTTP controller, a SQL repository, a payment provider's SDK, a command-line script, an in-memory fake. One port can have many adapters. The application inside the hexagon knows only its ports.
The hexagon has no meaning as a shape; Cockburn chose it to escape the top-to-bottom picture of layers and to leave room to draw several ports on each side. What matters is the inside/outside boundary and the asymmetry between two kinds of port.
Driving and driven ports
Driving and driven sides
Driving (primary) ports and adapters sit on the left of the usual picture. A driving adapter receives a request from a primary actor (a user's browser, a mobile app, another system, a test, a scheduled job) and calls a driving port, which is the application's own API: in practice a use case such as PlaceOrder. The adapter turns HTTP, a message or a command-line argument into a call with domain values, and turns the result back into a response.
Driven (secondary) ports and adapters sit on the right. The application calls them when it needs something from the outside world: storing an order, charging a card, sending a notification, reading the time. The port is an interface owned by the application and written in its terms (OrderRepository.save(order)), and a driven adapter implements it for a technology (PostgreSQL, a card provider's API, an SMS gateway). This is where dependency inversion happens: at run time the application calls the adapter, but in the source code the adapter depends on the application's interface, not the other way round.
Cockburn's original text speaks of primary and secondary actors; the terms driving and driven are now more common, and he uses them himself. The distinction matters for testing: driving ports are exercised by test harnesses acting as users, and driven ports are replaced by test doubles. It also matters for change: new channels (a voice assistant, a partner API) arrive as new driving adapters, and new infrastructure arrives as new driven adapters.
Someone has to create the adapters and plug them into the application. That happens in a single composition root, typically the program's start-up code, which is the only part of the system that knows about every adapter. A dependency-injection framework can do this work, but plain constructor calls in main are often clearer.
Onion architecture
Jeffrey Palermo described the onion architecture in a series of blog posts starting in July 2008, aimed at long-lived business applications with substantial behaviour (he advised against it for small websites). He drew concentric rings: the domain model at the centre, domain services around it, application services around those, and an outer ring containing the user interface, infrastructure (including the database) and tests. His key tenets are that the application is built around an independent object model, that inner layers define interfaces which outer layers implement, that all coupling is towards the centre, and that the application core can be compiled and run separately from infrastructure.
Compared with hexagonal architecture, onion adds named rings inside the application and makes explicit a point Cockburn's picture leaves implicit: the database is not the foundation of the system but an external service at the edge. Repository interfaces live in the core; their implementations live in the infrastructure ring.
Clean architecture and the dependency rule
Robert C. Martin published "The Clean Architecture" on his blog in August 2012 as a synthesis of hexagonal architecture, onion architecture and other similar proposals, and expanded it into the book Clean Architecture in 2017. His picture has four rings. Entities hold enterprise-wide business rules. Use cases hold application-specific rules that orchestrate entities. Interface adapters convert data between the form the use cases need and the form external agencies need: controllers, presenters and gateways. Frameworks and drivers form the outer ring: the web framework, the database, the UI.
The dependency rule ties it together: source code dependencies point only inwards, and nothing in an inner ring may name anything declared in an outer ring, whether a class, a function or a data format. When control must flow outwards (a use case saving an order), the inner ring declares an interface and the outer ring implements it. Data that crosses a boundary should be simple structures in the inner ring's terms, never a database row or an HTTP request object. Martin also argues that the top-level structure should reveal the system's purpose (orders, payments, deliveries) rather than its framework, an idea he calls screaming architecture.
The clean architecture rings
Hexagonal, onion and clean architecture compared
| Aspect | Hexagonal (ports and adapters) | Onion | Clean |
|---|---|---|---|
| Originator and date | Alistair Cockburn; written up as an article in 2005 | Jeffrey Palermo; blog series from July 2008 | Robert C. Martin; blog post August 2012, book 2017 |
| Picture | A hexagon with an inside (application) and an outside (adapters), left side driving, right side driven | Concentric rings: domain model, domain services, application services, then UI, infrastructure and tests | Concentric rings: entities, use cases, interface adapters, frameworks and drivers |
| Central rule | The application talks to the world only through ports; adapters translate | All coupling is towards the centre; the core defines interfaces that the outer ring implements | The dependency rule: source code dependencies point only inwards |
| Internal structure prescribed | None: only inside versus outside | Yes: named inner rings for the domain | Yes: entities and use cases, plus guidance on presenters and data crossing boundaries |
| Main emphasis | Symmetry: users, tests, scripts and other programs can all drive the application | Putting the domain model, not the database, at the centre of a business application | Independence from frameworks, UI, database and external agencies; testable business rules |
| Typical vocabulary | Port, adapter, driving (primary), driven (secondary), actor | Domain model, domain services, application services, infrastructure | Entity, use case (interactor), gateway, presenter, controller |
A use case with a port and two adapters
The following Python example implements the food-delivery platform's PlaceOrder use case in the hexagonal way. The first snippet is the inside: a small domain class, two driven ports written as typing.Protocol classes, and the use case, which acts as the driving port. The second snippet is the outside: a PostgreSQL adapter and an in-memory test double that implement the same port. The third shows why it matters: a fast unit test, and the composition root where production adapters are plugged in.
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol
@dataclass
class Order:
id: str
total: Decimal
status: str = "new"
class OrderRepository(Protocol): # driven port
def save(self, order: Order) -> None: ...
class PaymentGateway(Protocol): # driven port
def charge(self, order_id: str, amount: Decimal) -> bool: ...
class PlaceOrder: # driving port + use case
def __init__(self, orders: OrderRepository, payments: PaymentGateway):
self.orders, self.payments = orders, payments
def __call__(self, order_id: str, total: Decimal) -> Order:
if total <= 0:
raise ValueError("empty basket")
order = Order(order_id, total)
order.status = "paid" if self.payments.charge(order_id, total) else "payment-failed"
self.orders.save(order)
return orderclass PostgresOrderRepository: # real driven adapter
def __init__(self, conn):
self.conn = conn
def save(self, order):
with self.conn.cursor() as cur:
cur.execute(
"INSERT INTO orders (id, total, status) VALUES (%s, %s, %s) "
"ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status",
(order.id, order.total, order.status))
self.conn.commit()
class InMemoryOrderRepository: # test double, same port
def __init__(self):
self.saved = {}
def save(self, order):
self.saved[order.id] = orderclass DecliningPayments:
def charge(self, order_id, amount): return False
def test_failed_payment_is_recorded():
orders = InMemoryOrderRepository()
place = PlaceOrder(orders, DecliningPayments())
place("A-17", Decimal("23.50"))
assert orders.saved["A-17"].status == "payment-failed"
# composition root (main.py): the only place that knows every adapter
# place_order = PlaceOrder(PostgresOrderRepository(conn), CardProviderGateway(key))
# app.add_route("/orders", make_rest_controller(place_order)) # driving adapterNotice what the use case does not contain: no SQL, no HTTP status codes, no provider-specific exception types, no framework decorators. The test runs in milliseconds and covers a case (a declined payment) that would be awkward to arrange against a real provider's sandbox. The PostgreSQL adapter needs its own tests, but those are focused integration tests of one class against a real database, not tests of business rules. This is the testability benefit described in Section 2.6: controllability and observability through injected dependencies.
Worked example
Worked example: swapping the payment provider
The food-delivery platform must move from one card provider to another within six weeks because of a pricing change. Compare the work in two versions of the system: one where checkout code calls the old provider's SDK directly in 23 places, and one built with a PaymentGateway driven port and an adapter.
| Direct calls: find the impact | Search finds the SDK in checkout, refunds, subscriptions and three admin screens. The SDK's own exception types are caught in 11 places, and its Charge object is stored in a database column. |
|---|---|
| Direct calls: the change | Every call site changes, plus a data migration for the stored objects. Every test that touched payments was written against the old SDK's sandbox and must be rewritten. Estimate: 5 to 6 weeks, with a risky all-at-once switch. |
| Ports and adapters: find the impact | One adapter class (about 200 lines) implements PaymentGateway. The core only knows charge, refund and the core's own PaymentResult type. |
| Ports and adapters: the change | Write a second adapter for the new provider; run the existing contract tests for the port against it; choose the adapter per order with a feature flag so that 1%, then 10%, then 100% of payments use the new one. Estimate: 2 weeks, reversible at every step. |
| What the port cost | Designing the port and adapter at the start took about 3 extra days, and every new payment feature needs a small change to the port as well as the adapter. |
The value of a port is proportional to the probability and cost of the change it isolates. Payment providers, message brokers and third-party APIs change often; your own core database engine may never change, and wrapping it in elaborate ports is harder to justify.
The port must speak the domain's language (charge, PaymentResult), not the old provider's. A port that mirrors one vendor's API protects nothing.
Characteristics of the ports-and-adapters family
| Characteristic | Rating | Reason |
|---|---|---|
| Testability | High | Business rules can be tested in memory, fast and deterministically, by plugging test doubles into the driven ports. |
| Modifiability of infrastructure | High | Replacing a database, payment provider or web framework means writing a new adapter; the core does not change. |
| Modifiability of business rules | Medium to high | Rules live in one place, free of framework code, but a feature still crosses port, use case and adapter. |
| Simplicity | Low to medium | More types, interfaces and mapping code than a straightforward layered application; harder for newcomers at first. |
| Development speed (early) | Medium | Extra structure slows the first features of a simple CRUD application; it pays back as the domain grows. |
| Overall cost | Cheap to medium | No extra infrastructure; the cost is developer time spent writing and maintaining ports, adapters and mapping code. |
| Performance | Medium to high | Interface calls cost almost nothing; the real overheads are object mapping between layers and losing some ORM conveniences. |
| Scalability, deployability, fault tolerance | Not decided by this style | These depend on the deployment style (monolith or services) that the hexagon sits in. |
Costs, criticism and when to use it
The benefits are real, and so are the costs. Every port is an extra interface; every boundary may need mapping code to convert between, say, an ORM entity, a domain object and an API response. For an application that mostly moves form fields into tables with little logic in between, this structure adds files without adding protection, and a framework's conventions (an active-record model, a generated admin interface) may give a better result faster. Some experienced practitioners argue that strict versions of clean architecture produce too much indirection and that most teams never swap their database; others reply that the main payoff is not swapping technology but fast, reliable tests and business code that can be read without knowing the framework. Both are right in different contexts.
Use it when the business logic is substantial and will outlive the current frameworks; when the system integrates with external services that are likely to change; when fast, isolated tests of business rules are important; and as the internal structure of modules in a modular monolith or of services built with domain-driven design (Section 5.7). Be cautious with simple CRUD applications, prototypes and short-lived tools, where the framework's default structure is usually good enough. A pragmatic middle path is common: ports for external services and anything likely to change, and the framework's own conveniences elsewhere.
Enforcing the dependency rule
As with layers, the rule erodes unless a machine checks it. In Python, import-linter can declare the rings as a layers contract (adapters above application above domain) and add forbidden contracts that stop the domain and application packages importing web frameworks, database drivers or SDKs. In Java, ArchUnit has a ready-made onionArchitecture() rule in which you name the domain model, domain service, application service and adapter packages, and it fails a test if any dependency points outwards. Splitting the code into separate build modules (a domain module with no framework dependencies at all) makes outward dependencies impossible to compile, which is the strongest check of all. These checks are examples of the fitness functions covered in Chapter 11.
A package structure that makes the rule visible helps reviewers: for example orders/domain, orders/application (use cases and ports) and orders/adapters/{rest,postgres,payments}. Organising by business area first and ring second keeps the structure aligned with the domain boundaries of sections 5.3 and 5.7.
Check your understanding
Is a REST controller a driving or a driven adapter? And a SQL repository?
A REST controller is a driving adapter: it receives requests and calls the application. A SQL repository is a driven adapter: it implements an interface that the application calls.
In clean architecture, may a use case return an ORM entity to a controller?
No. The ORM belongs to the outer ring, so a use case that names it has an outward dependency. It should return a simple structure or domain object defined in an inner ring.
Where are adapters created and connected to the application?
In a composition root, usually the start-up code, which is the only place that knows about every adapter.
What does onion architecture add to the hexagonal picture?
Named rings inside the application (domain model, domain services, application services) and the explicit statement that the database is an external concern at the edge, not the foundation.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “Our Django app has business rules spread across models, views and Celery tasks. How could we introduce ports and adapters gradually without a rewrite?”