Isolate core domain logic behind ports, so the database, UI, and external services are all swappable adapters plugged into it.
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.
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.
`placeOrder` only depends on the `OrderRepo` port's shape — a Postgres adapter and an in-memory test adapter both satisfy it identically.
// 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"]);