Hexagonal Architecture — Ports and Adapters Pattern (2026)
In this tutorial, you'll learn Hexagonal Architecture (Ports and Adapters) — how to decouple your core business logic from infrastructure using ports as interfaces and adapters as implementations. Why does this matter? When database drivers, message queues, or web frameworks change, your core logic should remain untouched. Real-world use: companies like ThoughtWorks and Netflix use hexagonal designs to swap databases, add new API protocols, and test business logic without spinning up infrastructure.
What Is Hexagonal Architecture?
Hexagonal Architecture, introduced by Alistair Cockburn in 2005, places the domain logic at the center of a hexagon. Each side of the hexagon represents a port — an interface that defines how the outside world communicates with the application. Adapters on the outside implement those ports, connecting to specific technologies like databases, web servers, or message queues.
The shape isn't literal — the hexagon simply provides multiple connection points without implying a hierarchical top-down structure like layered architecture.
graph TB
subgraph Adaptors[Adapters]
PWA[Web Controller]
CLI[CLI]
GUI[GUI]
end
subgraph Ports[Ports]
IN[Inbound Ports
Drive the app]
OUT[Outbound Ports
Driven by the app]
end
subgraph Core[Core Domain]
UC[Use Cases]
ENT[Entities]
end
PWA --> IN
CLI --> IN
GUI --> IN
IN --> UC
UC --> ENT
UC --> OUT
OUT --> DB[(Database)]
OUT --> MQ[(Message Queue)]
OUT --> API[(External API)]
style Core fill:#E74C3C,color:#fff
style Ports fill:#E67E22,color:#fff
style Adaptors fill:#4A90D9,color:#fff
How Hexagonal Architecture Works
Driving and Driven Ports
There are two categories:
- Driving (Primary) Ports — inbound interfaces that drive the application. Controllers, CLI handlers, and message listeners use these to invoke use cases.
- Driven (Secondary) Ports — outbound interfaces that the application drives. Repository interfaces, message publishers, and API clients implement these.
Ports Are Interfaces
Ports belong to the core and define what the application needs:
from abc import ABC, abstractmethod
from dataclasses import dataclass
# --- Core Domain (inside the hexagon) ---
@dataclass
class Order:
id: str
items: list[str]
total: float
# Driven port — defined by the core
class OrderRepository(ABC):
@abstractmethod
async def save(self, order: Order) -> None: ...
@abstractmethod
async def find_by_id(self, order_id: str) -> Order | None: ...
# Use case — drives the logic
class CreateOrderUseCase:
def __init__(self, repo: OrderRepository):
self._repo = repo # depends on port, not implementation
async def execute(self, items: list[str], total: float) -> Order:
order = Order(id=uuid4(), items=items, total=total)
await self._repo.save(order)
return order
Adapters Implement Ports
Adapters live outside the hexagon and implement the ports:
# Adapter — PostgreSQL implementation of OrderRepository
import asyncpg
class PostgresOrderRepository(OrderRepository):
def __init__(self, pool: asyncpg.Pool):
self._pool = pool
async def save(self, order: Order) -> None:
async with self._pool.acquire() as conn:
await conn.execute(
"INSERT INTO orders VALUES ($1, $2, $3)",
order.id, order.items, order.total
)
async def find_by_id(self, order_id: str) -> Order | None:
async with self._pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT * FROM orders WHERE id = $1", order_id
)
return Order(**row) if row else None
Driving Adapter
A driving adapter (controller) brings input into the hexagon:
from fastapi import FastAPI, HTTPException
app = FastAPI()
repo = PostgresOrderRepository(pool)
use_case = CreateOrderUseCase(repo)
@app.post("/orders")
async def create_order(items: list[str], total: float):
try:
order = await use_case.execute(items, total)
return {"id": order.id, "total": order.total}
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
Dependency Flow
sequenceDiagram
participant W as Web Adapter
participant UC as Use Case (Core)
participant R as Repository Port
participant D as Database Adapter
W->>UC: execute(request)
UC->>R: save(order)
R->>D: INSERT INTO orders ...
D-->>R: OK
R-->>UC: void
UC-->>W: Order
Real-World Examples
Spring Boot + JPA
Spring's @Repository beans implement driven ports, @Service beans contain use cases, and @RestController beans act as driving adapters. The core domain package has zero Spring imports.
Node.js + Express
Express routes are driving adapters. Repository classes implement interfaces (duck-typed or with TypeScript interfaces) that the service layer depends on.
Python + FastAPI
FastAPI routes as driving adapters, SQLAlchemy repositories as driven adapters, and Pydantic schemas as data transfer objects crossing the port boundary.
Pros and Cons
| Pros | Cons |
|---|---|
| Testability — test use cases with mock adapters | Indirection — interfaces everywhere add complexity |
| Technology isolation — swap databases without touching core | Over-engineering — CRUD apps don't need this flexibility |
| Parallel work — implement multiple adapters independently | Bootstrap cost — wiring everything together takes time |
| Replaceable infrastructure — databases, message queues, APIs are all replaceable | Team learning — developers must understand the pattern |
| Clear boundaries — everyone knows where code belongs | Runtime overhead — adapter transformation layers can slow things |
When to Use Hexagonal Architecture
Hexagonal Architecture fits when:
- Multiple delivery mechanisms — REST API plus CLI plus message consumer
- Database migrations — you plan to switch from PostgreSQL to DynamoDB
- Complex business logic — domain rules that should be tested independently
- Microservice design — each service has clear ports for inter-service communication
Skip it for simple CRUD APIs, prototypes, or when the database and framework are permanent choices.
FAQ
Related Concepts
- Clean Architecture — concentric circles version of the same idea
- MVC Architecture — controllers as driving adapters
- Repository Pattern — common driven port pattern
- Dependency Injection — wiring ports to adapters
Practice Questions
What is the difference between a driving port and a driven port?
Why must ports be defined in the core domain, not in the adapter layer?
How does Hexagonal Architecture make database migrations safer?
Can a Hexagonal application have more than one driving adapter?
What is the relationship between Hexagonal Architecture and the Dependency Inversion Principle?
Challenge
Take a monolithic controller that directly calls SQLAlchemy and returns JSON. Identify the core use case. Extract a repository port into the core, create an adapter class, and refactor the controller to call the use case through a driving port.
Real-World Task
Examine an existing service. Draw its current architecture — note every external dependency (database, queue, API). Define ports for each dependency. Implement one port swap (e.g., replace one SQL query with an in-memory adapter) and verify all tests pass without changes to the core logic.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro