Ports and Adapters (Hexagonal Architecture)
Isolates core logic behind interfaces (ports) it depends on, with concrete I/O implementations (adapters) plugged in from outside.
Purpose
Ports-and-adapters (hexagonal architecture) isolates a system’s core logic from its I/O concerns — the core depends only on abstract interfaces (ports); concrete implementations (adapters) plug in from outside, swappable without touching the core.
Example
// Core logic — depends on a port shape, not a concrete implementation
async function placeOrder(orderRepository, order) {
validateOrder(order);
await orderRepository.save(order);
return order;
}
// Adapter: production
const postgresOrderRepository = {
save: (order) => db.query('INSERT INTO orders ...', [order]),
};
// Adapter: tests
const inMemoryOrderRepository = {
save: (order) => testOrders.push(order),
};
Behavior notes
- The same dependency-inversion principle used for a single function’s testability, applied at module or service-boundary scale — recall Dependency Inversion.
- A port can be defined implicitly (a shared shape/contract, common in plain JavaScript) or explicitly (a TypeScript interface).
- Swapping an adapter — a new database, a different email provider — never requires changing the core logic that depends on the port, only writing a new adapter matching the same shape.
Common mistakes
- Leaking I/O-specific details (a database-specific error type, a library-specific object shape) through the port, coupling the “core” logic to a specific adapter anyway.
- Over-applying the pattern to a small, simple system where the added indirection isn’t earning its cost yet.
Runtime notes
An architectural pattern, not a language or framework feature — applies identically regardless of runtime or specific technology choices.