<arch.design/>
Principles/Hexagonal Architecture
{ }CodeArchitectureintermediate2005ports-and-adapterscockburndomain-isolationtestability

Hexagonal Architecture

Place the domain at the centre and connect all I/O (HTTP, database, messaging) through explicit ports and adapters — so the core is framework-free and infinitely testable.

4/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

Hexagonal Architecture (also called Ports & Adapters) was defined by Alistair Cockburn in 2005. The core idea: your application has an inside (the domain) and an outside (everything else). The two communicate exclusively through ports — interfaces owned by the domain — and adapters that implement those interfaces.

Ports come in two flavours. Driving ports (input/left side) define how the outside world calls into the domain — e.g. PlaceOrderUseCase. An HTTP controller is a driving adapter: it translates HTTP to a command and calls the port. Driven ports (output/right side) define what the domain needs from the outside — e.g. OrderRepository. A Postgres repository is a driven adapter: it implements the port.

Because the domain owns the port interfaces and knows nothing about HTTP, SQL, or message queues, you can swap any adapter — REST for gRPC, Postgres for DynamoDB — by writing a new adapter class. Tests drive the domain directly through its ports, with in-memory driven adapters, no framework or database required.

Project structure

recommended layout
project structure
src/
├── domain/ # Core — ZERO external dependencies
│ ├── order.ts # Entity + domain rules
│ └── order-service.ts # Orchestrates domain logic
├── ports/
│ ├── in/ # Driving ports (what callers use)
│ │ └── place-order.port.ts
│ └── out/ # Driven ports (what domain needs)
│ ├── order-repository.port.ts
│ └── notification.port.ts
└── adapters/
├── http/ # Driving adapter — calls domain
│ └── order-controller.ts
└── db/ # Driven adapter — called by domain
└── pg-order-repository.ts

Implementation

TypeScript · Go · Rust
// ports/in/place-order.port.ts — what the outside world calls
export interface PlaceOrderUseCase {
  execute(cmd: PlaceOrderCommand): Promise<string>;
}

// ports/out/order-repository.port.ts — what the domain needs
export interface OrderRepository {
  save(order: Order): Promise<void>;
  findById(id: string): Promise<Order | null>;
}

// domain/order-service.ts — pure logic, imports nothing external
export class OrderService implements PlaceOrderUseCase {
  constructor(private repo: OrderRepository) {}  // ← driven port

  async execute(cmd: PlaceOrderCommand): Promise<string> {
    const order = Order.create(cmd.customerId, cmd.items);
    order.validate();           // domain rule — throws if invalid
    await this.repo.save(order);
    return order.id;
  }
}

// adapters/http/order-controller.ts — driving adapter
export class OrderController {
  constructor(private useCase: PlaceOrderUseCase) {}  // ← driving port

  async post(req: Request): Promise<Response> {
    const id = await this.useCase.execute(req.body);
    return Response.json({ orderId: id }, { status: 201 });
  }
}

// adapters/db/pg-order-repository.ts — driven adapter
export class PgOrderRepository implements OrderRepository {
  async save(order: Order)           { /* SQL INSERT */ }
  async findById(id: string)         { /* SQL SELECT */ }
}

// bootstrap: plug adapters into the hex
const repo       = new PgOrderRepository(db);
const service    = new OrderService(repo);        // domain + driven
const controller = new OrderController(service);  // driving + domain

Why it matters

Frameworks and databases age faster than business logic. Hexagonal Architecture ensures your domain — where real value lives — stays clean and portable as technology changes around it.

When to use

  • Long-lived applications where technology choices may evolve
  • Teams that want exhaustive domain testing without infrastructure setup
  • Systems that need multiple delivery mechanisms (HTTP, CLI, gRPC, queues)
  • When practising DDD — ports align naturally with domain service boundaries

When NOT to use

  • Simple CRUD services with no real domain logic — pure overhead
  • Short-lived scripts or MVPs where speed of delivery trumps structure

Trade-offs

+

Domain is 100% framework-free — test without running a server or DB

Port/adapter abstraction doubles the number of files for each I/O boundary

+

Adapters are interchangeable — swap delivery or persistence transparently

Requires discipline; teams without experience can create wrong-level abstractions

+

Multiple delivery mechanisms (HTTP + CLI + gRPC) with one domain

Higher initial complexity vs a simple layered approach

In production

Netflix

Recommendation domain isolated behind ports; multiple delivery adapters serve different clients

Axon Framework

Java CQRS/ES framework built on hexagonal principles — ports for commands, queries, and events

Growing Django apps

Clean Architecture / hexagonal layering increasingly adopted to escape Django ORM lock-in

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles