Architectural · App Structure · DWG NO. 044

Hexagonal Architecture

Isolate core domain logic behind ports, so the database, UI, and external services are all swappable adapters plugged into it.

TypeApplication Structure
ScopeWhole application
ComplexityMedium–High
Common inDomain-heavy services, testable business logic, tech-agnostic cores

Business logic tangled with the database and framework it happens to run on

When domain logic calls the ORM directly, imports framework request objects, and writes straight to a specific database's client, swapping the database — or even unit-testing the business rule without one — means untangling code that was never supposed to know any of that existed.

Ports define what the domain needs; adapters provide it

The domain core defines ports — interfaces describing what it needs ("save an order") without saying how. Adapters implement those ports for a specific technology (a Postgres adapter, an in-memory adapter for tests). The core never imports an adapter directly; adapters are plugged in from the outside.

Domain CorePort: OrderRepoPostgres AdapterPort: NotifierEmail Adapter

A domain core with a swappable repository port

`placeOrder` only depends on the `OrderRepo` port's shape — a Postgres adapter and an in-memory test adapter both satisfy it identically.

hexagonal-order.js
// Port: what the domain needs, not how it's done.
// (In JS this is just a shape/contract — any object with save() satisfies it.)

// Domain core — knows nothing about Postgres, or anything else concrete.
function placeOrder(orderRepo, items) {
  if (items.length === 0) throw new Error("order must have items");
  const order = { id: crypto.randomUUID(), items, status: "placed" };
  orderRepo.save(order);
  return order;
}

// Adapter #1: real database.
const postgresAdapter = {
  save: (order) => console.log(`postgres: INSERT order ${order.id}`),
};

// Adapter #2: in-memory, used in tests — same port, zero domain changes.
const inMemoryAdapter = {
  store: new Map(),
  save(order) { this.store.set(order.id, order); },
};

placeOrder(postgresAdapter, ["book"]);
placeOrder(inMemoryAdapter, ["pen"]);

Trade-offs