Hexagonal Architecture#
The hexagonal (ports and adapters) pattern places the application's business logic at the centre, surrounded by ports (interfaces describing what it needs) and adapters (implementations that talk to specific technologies). The core never depends on infrastructure; infrastructure depends on the core.
flowchart LR
HTTP[HTTP adapter] -->|inbound port| Core
CLI[CLI adapter] -->|inbound port| Core
Test[Test adapter] -->|inbound port| Core
Core((Application core<br/>domain + use cases)) -->|outbound port| DB[DB adapter]
Core -->|outbound port| MQ[Queue adapter]
Core -->|outbound port| Email[Email adapter]
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
class Core service;
class HTTP,CLI,Test edge;
class DB,MQ,Email datastore;
Swap any adapter without touching the core. Test the core with in-memory adapters; deploy with real ones.
Alistair Cockburn's hexagonal pattern (also called ports and adapters) is one answer to "how do I keep the business rules free of framework noise?" The technique is simple: define what the application needs (ports), then attach concrete implementations (adapters).
Driving vs driven ports#
classDiagram
class ApplicationCore {
+placeOrder(cmd)
+getOrder(id)
}
class DrivingPort {
<<interface>>
+placeOrder(cmd)
}
class DrivenPort {
<<interface>>
+saveOrder(o)
+publishEvent(e)
}
class HttpAdapter
class PostgresAdapter
class KafkaAdapter
DrivingPort <|.. ApplicationCore
HttpAdapter --> DrivingPort
ApplicationCore --> DrivenPort
PostgresAdapter ..|> DrivenPort
KafkaAdapter ..|> DrivenPort
- Driving (inbound) port: the API the outside world uses to invoke the core. HTTP, CLI, scheduled job.
- Driven (outbound) port: the API the core uses to talk to external systems. Repository, message publisher, email sender.
The core defines both port interfaces; adapters implement (driven) or call (driving).
A concrete example#
# Driving port - what the world can ask of us
class PlaceOrder(Protocol):
def execute(self, cmd: PlaceOrderCommand) -> OrderId: ...
# Driven port - what we need from the world
class OrderRepository(Protocol):
def save(self, o: Order) -> None: ...
def get(self, id: OrderId) -> Order | None: ...
# Application core
class PlaceOrderService(PlaceOrder):
def __init__(self, repo: OrderRepository, bus: EventPublisher):
self.repo = repo
self.bus = bus
def execute(self, cmd):
order = Order.create(cmd.items)
self.repo.save(order)
self.bus.publish(OrderPlaced(order.id))
return order.id
# HTTP adapter
@app.post("/orders")
def http_place_order(payload, svc: PlaceOrder):
return {"id": svc.execute(PlaceOrderCommand.from_payload(payload))}
# DB adapter
class PostgresOrderRepository(OrderRepository):
def save(self, o): ... # SQL here
Composition root#
Adapters are wired together in one place - the composition root - usually at process startup:
def build_app():
db = PostgresOrderRepository(conn_str=os.environ["DATABASE_URL"])
bus = KafkaPublisher(brokers=os.environ["KAFKA_URL"])
svc = PlaceOrderService(repo=db, bus=bus)
return FastAPI().include_router(make_http_router(svc))
The core never imports framework, DB driver, or message library. Frameworks live at the edges.
Testing strategy#
flowchart TB
Test[Test runner] -->|driving port| Core((PlaceOrderService))
Core -->|driven port| Fake[InMemoryRepository<br/>FakeEventPublisher]
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
class Core service;
class Test,Fake edge;
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
Unit tests substitute fakes for driven adapters - no DB, no broker, no HTTP server.
Integration tests run real adapters against test containers.
Relationship to clean / onion architectures#
| Pattern | What | Differences |
|---|---|---|
| Hexagonal | Cockburn 2005 | Ports and adapters; symmetric inbound/outbound |
| Onion | Palermo 2008 | Concentric layers; domain at the centre |
| Clean | Martin 2012 | Same idea + use-case / interface-adapter / entity layers |
All three share the dependency rule: source code dependencies point inward toward the domain.
When it's overkill#
- Throwaway scripts, glue code, internal tools.
- Apps that will never replace their DB or HTTP framework.
- Teams unfamiliar with dependency inversion - the indirection costs more than it pays.
When it pays off#
- Long-lived services (years, not months).
- Heavy testing requirements (regulated, financial, healthcare).
- Likely substitutions: changing DBs, swapping cloud providers, moving from REST to gRPC.
How hexagonal composes#
flowchart TB
HX((Hexagonal<br/>architecture))
CA[Clean architecture<br/>concentric layers cousin]
DI[Dependency injection<br/>wiring technique]
RP[Repository pattern<br/>canonical driven port]
DDD[DDD tactical<br/>core types]
CA -. cousin .- HX
DI --> HX
HX --> RP
DDD --> HX
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class HX service;
class CA,DI,RP,DDD datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
LLD |
Clean architecture | concentric layers cousin | clean-architecture |
LLD |
Dependency injection | how adapters get into the core | dependency-injection |
LLD |
Repository pattern | a canonical driven port | repository-pattern |
LLD |
DDD tactical | domain primitives that live in the core | ddd-tactical |
Quick reference#
Terms#
| Term | Meaning |
|---|---|
| Driving port | Inbound API the world calls |
| Driven port | Outbound API the core needs |
| Adapter | Concrete implementation of a port |
| Composition root | Single place that wires adapters |
Rules#
- Core defines port interfaces
- Adapters depend on core (never reverse)
- No framework, DB, or HTTP code inside core
- One composition root per process
Testing#
- Unit: substitute fakes for driven ports
- Integration: real adapters + test containers
- Contract: verify adapters honour port semantics
When to use#
- Long-lived services
- Heavy testing requirements (regulated)
- Likely substitutions (DB swap, vendor change)
When not to use#
- Throwaway scripts, glue code
- Apps that will never change adapters
- Teams unfamiliar with DI
Related#
- Clean architecture, Onion architecture - same dependency rule, different framing.
Refs#
- Cockburn - Hexagonal Architecture (2005)
- Martin - Clean Architecture
- Netflix tech blog
FAQ#
What is hexagonal architecture in simple terms?#
It places business logic at the centre with ports describing what it needs. Adapters then provide HTTP, database, or queue implementations of those ports.
What is the difference between hexagonal and clean architecture?#
Both put the domain at the centre with dependencies pointing inward. Clean architecture adds more concentric rings (entities, use cases, interface adapters), while hexagonal keeps just core and adapters.
What is a port versus an adapter?#
A port is an interface defined by the core, like OrderRepository or PaymentGateway. An adapter is the concrete implementation, like PostgresOrderRepository or StripePaymentGateway.
Does hexagonal architecture work for small services?#
Yes, but you can keep it lightweight. Even one driving port (HTTP) and one driven port (DB) gives you the ability to test the core with no infrastructure.
How does hexagonal architecture help testing?#
You can run the core against in-memory adapters at full speed without spinning up Postgres or Kafka. Integration tests then verify each adapter against the real system.
Related Topics#
- Clean Architecture: concentric-layer cousin with the same dependency rule
- Dependency Injection: the wiring technique that joins adapters to the core
- Repository Pattern: the most common driven port