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/5Inside 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 layoutsrc/├── 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 + domainWhy 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
Recommendation domain isolated behind ports; multiple delivery adapters serve different clients
Java CQRS/ES framework built on hexagonal principles — ports for commands, queries, and events
Clean Architecture / hexagonal layering increasingly adopted to escape Django ORM lock-in
Industry adoption
Related principles
Clean Architecture
LiveOrganise code into concentric dependency rings so business logic never depends on frameworks, databases, or UI.
Repository Pattern
Decouple business logic from data access by defining a collection-like interface for retrieving and persisting domain objects.
Dependency Injection
Supply a component's dependencies from the outside rather than letting it construct them — so implementations can be swapped without changing the component.
Domain-Driven Design
Model software around the core business domain using a shared language between developers and domain experts.