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.
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.
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
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
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
Protected core
Framework and library churn stays at the edges. The domain outlives the web framework of the year. - 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 adapters | With ports and adapters | |
|---|---|---|
| Business rule test | Needs a database and an HTTP stack | Plain unit test with fakes |
| Change payment provider | Touches domain code everywhere it calls the SDK | New adapter implementing the same port |
| Add a message-driven entry point | Duplicate controller logic | New driving adapter calling the same use case |
| Split a module into a service | Untangle first | Replace in-process adapters with network adapters |
| Cost | Fast to start | More files and indirection; worth it where logic and change live |