Skip to content

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.

Hexagonal architecture: application core surrounded by ports and adapters
Source: Wikimedia Commons. CC BY-SA 4.0.
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
  • 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.

Further reading#