Back to the map

Module 11 · Architectural patterns · Lesson 7 of 9

Hexagonal architecture

Ports and adapters: the domain in the middle, interfaces it defines at the edges, and adapters that plug infrastructure into them. Why it makes a service testable and swappable, how it relates to clean and onion architecture.

DeepNice-to-knowSenior 11 minPro Updated Nov 2026

Why it matters

Most of the architecture in this track is about boxes and arrows between services. Hexagonal architecture is about the inside of one box, and it matters to system design because a service whose business logic is tangled with its database driver, HTTP framework and message client cannot be tested without all of them, cannot swap any of them, and cannot be split later along the lines the modular-monolith lesson recommended. The pattern is how you keep a service's core independent of the things that change most.

The idea

Hexagonal architecture (ports and adapters) puts the domain, the business rules and models, at the centre with no dependencies on anything outside it. The domain defines ports: interfaces describing what it needs (a repository to store orders, a payment gateway, a clock) and what it offers (use cases such as place order). Adapters implement the ports for specific technologies: a Postgres repository, a Stripe gateway, an HTTP controller that calls the use case, a Kafka consumer that does the same. Dependencies point inward: adapters depend on the domain's interfaces; the domain depends on nothing.

HTTP controllerKafka consumertest harnessdomainuse cases · entities · portsPostgres repoStripe gatewayin-memory fake
The domain defines ports; adapters on the left drive it (HTTP, CLI, message consumers), adapters on the right are driven by it (database, payment provider, broker). Swap any adapter without touching the centre.

Driving and driven sides

Driving (primary) adapters call into the domain: an HTTP handler translates a request into a use-case call; a message consumer does the same from an event; a test does the same directly. Driven (secondary) adapters are called by the domain through ports: persistence, external services, time, randomness. The domain's code is identical whether it was triggered by HTTP or a queue and whether it stores to Postgres or memory, which is the property that makes everything else possible.

What it buys

  1. 1

    Testability

    Business rules are tested with in-memory fakes for the driven ports: fast, deterministic, no database or network. Adapters are tested separately against the real technology.
  2. 2

    Swappability

    Moving from one database or provider to another is a new adapter, not a rewrite. Adding a second entry point (an event consumer next to the HTTP API) reuses the same use cases.
  3. 3

    Protected core

    Framework and library churn stays at the edges. The domain outlives the web framework of the year.
  4. 4

    Extractability

    A module built this way already has explicit interfaces; splitting it into a service replaces in-process adapters with network ones.

Relatives

Clean architecture and onion architecture draw the same picture as concentric rings (entities, use cases, interface adapters, frameworks) with the same rule: dependencies point inward. Domain-driven design supplies the vocabulary for what goes in the centre (aggregates, entities, value objects, domain events) and bounded contexts for where one hexagon ends and the next begins. The differences between the named variants are mostly presentation; the shared idea is dependency inversion at the infrastructure boundary.

The pragmatic amount

Full ceremony for a CRUD endpoint is waste: a port, an adapter, a use case and a DTO mapping to save a row. Apply the pattern where there are real business rules and real infrastructure that may change: order placement, pricing, eligibility. Let thin endpoints be thin. Define ports in the domain's language (OrderRepository.save(order)), not the infrastructure's (PostgresClient.query(sql)). Keep mapping between domain models and persistence or transport models at the adapters so the centre never imports a framework type.

 Without ports and adaptersWith ports and adapters
Business rule testNeeds a database and an HTTP stackPlain unit test with fakes
Change payment providerTouches domain code everywhere it calls the SDKNew adapter implementing the same port
Add a message-driven entry pointDuplicate controller logicNew driving adapter calling the same use case
Split a module into a serviceUntangle firstReplace in-process adapters with network adapters
CostFast to startMore files and indirection; worth it where logic and change live
Hexagonal architecture · Backplane