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.

Taught in these lessons

Related reference

Updated 2026-09-03