A Quick Tour of Domain Design Patterns
A compact glossary for naming the structures that show up in a domain model
A lot of the value in domain modeling comes from having shared names for recurring structures. When two engineers both know what an Aggregate is, a design conversation that would take an hour takes five minutes. Several of these names come out of the work around Domain-Driven Design, a way of thinking about software popularized by Eric Evans, and a few are broader architectural ideas. Here is a quick tour I keep handy.
Service#
A Service is an operation exposed as a standalone interface in the model, with no state of its own. When a piece of behavior does not naturally belong to any single entity or value object — it coordinates several of them, or it represents a verb more than a noun — modeling it as a stateless Service keeps your entities from accumulating responsibilities that do not fit.
Reach for this when an operation feels like an action across multiple objects rather than something one object owns. A TransferFunds operation that touches two accounts is a Service; neither account should own it.
Aggregate#
An Aggregate is a cluster of related objects treated as a single unit for the purpose of changes. One entity inside the cluster is designated the root, and the root is the only entry point — outside code holds a reference to the root and never reaches past it to grab an inner object directly. This is how you keep invariants enforceable: every change goes through the root, so the root can guarantee the whole cluster stays consistent.
Reach for this when you have a group of objects that must stay consistent together. An Order and its line items form an Aggregate; you add or remove items through the Order, never by editing a line item in isolation.
Factory#
A Factory encapsulates creation when constructing an object, or a whole Aggregate, is complex enough that doing it inline would leak too much internal structure to the caller. Instead of scattering knowledge of how to assemble a valid object across the codebase, you put it in one place.
Reach for this when a constructor would need to know too much, or when valid construction has rules the caller should not have to remember. Building a fully wired Aggregate with all its invariants satisfied is the classic case.
Repository#
A Repository represents querying the datastore for all objects of a given type as if they were a conceptual in-memory collection. The domain code asks the Repository for the customers matching some criteria; it does not know or care whether that turned into a SQL query, a document lookup, or a call to a cache.
Reach for this when you want domain logic to talk about what it needs rather than how to fetch it. A CustomerRepository lets the rest of the model stay ignorant of persistence.
Unit of Work#
A Unit of Work groups database writes that must succeed or fail together and commits them as a single transaction. It tracks the changes you have made during some operation and flushes them atomically, so you never end up with half a change written and the other half lost.
Reach for this when one logical operation produces several writes that cannot be allowed to drift apart. Saving an order, decrementing inventory, and recording a payment should commit as one Unit of Work or not at all.
Functional Core, Imperative Shell#
This is an onion-style arrangement of a system. The core holds pure, side-effect-free logic — given the same inputs it always returns the same outputs and touches nothing outside itself. The outer shell holds all the I/O and side effects: reading the database, calling other services, writing files. The shell gathers inputs, hands them to the pure core, takes the result, and performs the side effects.
The payoff is testability and reasoning. The core is trivial to test because it is just functions, and the messy, hard-to-test parts are pushed to a thin outer layer.
Reach for this when business logic is tangled up with I/O and the tests are painful. Pull the decisions into a pure core, leave the doing in the shell.
None of these are rules you must apply everywhere. They are vocabulary. The real benefit is that once your team shares these names, you can point at a tangle in the code and say "that should be behind a Repository" or "those two objects want to be one Aggregate," and everyone knows exactly what you mean.