Skip to content

Hexagonal Architecture — Ports and Adapters Pattern (2026)

DodaTech Updated 2026-06-20 7 min read

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 overheadadapter transformation layers can slow things

When to Use Hexagonal Architecture

Hexagonal Architecture fits when:

  • Multiple delivery mechanismsREST 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

What is the difference between Hexagonal and Layered Architecture?

Layered Architecture stacks layers vertically (presentation → business → data). Hexagonal Architecture places the core at the center with ports on all sides, avoiding the rigid top-down dependency chain. Hexagonal makes it easier to add new entry points without modifying existing layers.

Is a port always an interface?

Yes. A port is always an interface or abstract class that belongs to the core domain. The implementation details (the adapter) are hidden behind that interface. This is the essence of dependency inversion.

How many ports should an application have?

One driving port per use case group, one driven port per external dependency. Most applications have 3-8 ports. Too many ports suggests over-fragmentation; too few suggests the design may not truly isolate the core.

Can Hexagonal Architecture live in a single module?

Yes. The separation can be package-based (e.g., core, adapters, config) within a single deployment unit. The module structure enforces the dependency direction. Modular monoliths benefit from hexagonal internal structure.

How do I test hexagonal applications?

Inject mock adapters for unit tests (test the core in isolation). Use real adapters with test databases for integration tests. Driving adapters (controllers) are tested with contract tests. This gives high confidence with fast execution

  • 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

  1. What is the difference between a driving port and a driven port?

  2. Why must ports be defined in the core domain, not in the adapter layer?

  3. How does Hexagonal Architecture make database migrations safer?

  4. Can a Hexagonal application have more than one driving adapter?

  5. 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