Be Software Architecture Wise
Be Wise Apps

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 captions
Listen 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

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

Hexagonal architecture: driving adapters on the left call driving ports of the application; the application calls driven ports on the right, which driven adapters implementDriving side (primary)Driven side (secondary)Applicationdomain + use casesPlaceOrderTrackOrderdriving portsOrdersPaymentsdriven portsREST controllerMobile app APITest harnessBatch scriptPostgreSQL repoIn-memory repoCard providerFake paymentsSolid arrows: adapter calls a port. Dashed arrows: adapter implements a port. All dependencies point inwards.
Driving adapters call the application through driving ports; the application calls driven ports, which driven adapters implement, so the database and payment provider are replaceable plug-ins.

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

Clean architecture: four concentric rings, entities at the centre, then use cases, interface adapters, and frameworks and drivers outside; source code dependencies point only inwardsEntitiesUse casesInterface adaptersFrameworks and driversOuter ring: web framework, ORM,database, message broker, UIInterface adapters: controllers,presenters, repository implementationsUse cases: application-specificrules, e.g. PlaceOrderEntities: enterprise-wide rules,e.g. Order, MoneyThe dependency rule: source codedependencies point only inwards
Each ring may depend only on rings inside it; entities depend on nothing, and frameworks and drivers sit at the edge where they can be replaced.

Hexagonal, onion and clean architecture compared

AspectHexagonal (ports and adapters)OnionClean
Originator and dateAlistair Cockburn; written up as an article in 2005Jeffrey Palermo; blog series from July 2008Robert C. Martin; blog post August 2012, book 2017
PictureA hexagon with an inside (application) and an outside (adapters), left side driving, right side drivenConcentric rings: domain model, domain services, application services, then UI, infrastructure and testsConcentric rings: entities, use cases, interface adapters, frameworks and drivers
Central ruleThe application talks to the world only through ports; adapters translateAll coupling is towards the centre; the core defines interfaces that the outer ring implementsThe dependency rule: source code dependencies point only inwards
Internal structure prescribedNone: only inside versus outsideYes: named inner rings for the domainYes: entities and use cases, plus guidance on presenters and data crossing boundaries
Main emphasisSymmetry: users, tests, scripts and other programs can all drive the applicationPutting the domain model, not the database, at the centre of a business applicationIndependence from frameworks, UI, database and external agencies; testable business rules
Typical vocabularyPort, adapter, driving (primary), driven (secondary), actorDomain model, domain services, application services, infrastructureEntity, 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 order
Inside the hexagon: a domain rule, two driven ports and the PlaceOrder use case. Nothing here imports a web framework, a database driver or a payment SDK.
class 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] = order
Outside the hexagon: a real adapter and a test double for the same port. The use case cannot tell them apart.
class 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 adapter
A unit test drives the use case directly, with no web server, database or network; production wiring happens in one place at start-up.

Notice 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 impactSearch 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 changeEvery 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 impactOne adapter class (about 200 lines) implements PaymentGateway. The core only knows charge, refund and the core's own PaymentResult type.
Ports and adapters: the changeWrite 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 costDesigning 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

CharacteristicRatingReason
TestabilityHighBusiness rules can be tested in memory, fast and deterministically, by plugging test doubles into the driven ports.
Modifiability of infrastructureHighReplacing a database, payment provider or web framework means writing a new adapter; the core does not change.
Modifiability of business rulesMedium to highRules live in one place, free of framework code, but a feature still crosses port, use case and adapter.
SimplicityLow to mediumMore types, interfaces and mapping code than a straightforward layered application; harder for newcomers at first.
Development speed (early)MediumExtra structure slows the first features of a simple CRUD application; it pays back as the domain grows.
Overall costCheap to mediumNo extra infrastructure; the cost is developer time spent writing and maintaining ports, adapters and mapping code.
PerformanceMedium to highInterface calls cost almost nothing; the real overheads are object mapping between layers and losing some ORM conveniences.
Scalability, deployability, fault toleranceNot decided by this styleThese 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.

Next · 5.7Domain-driven designFinding boundaries in the business domain: ubiquitous language, core, supporting and generic subdomains, bounded contexts, context-map relationships, event storming, aggregates, and a context map of the food-delivery platform.Continue →

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