Domain-Driven Design: modeling the business, not the database


Domain-Driven Design is not an architecture. It is not a framework. It is not a set of folders called entities/, repositories/, and services/. It is, in Eric Evans’ 2003 phrasing, an approach to developing software for complex needs by deeply connecting the implementation to an evolving model of the core business concepts. The interesting word in that sentence is deeply. DDD is a discipline for making the model in the code and the model in the domain expert’s head the same model — and for keeping them that way as both change.

That goal is modest to state and extremely demanding to execute. Most software does not need it. A CRUD application with five screens over a known schema does not have a domain worth modeling; it has data worth storing. DDD earns its keep where the business logic itself is the hard part — trading, insurance, logistics, healthcare, anywhere the rules are intricate, the vocabulary is specialized, and the cost of misunderstanding compounds.

Evans’ book is usually called the blue book. Vaughn Vernon’s 2013 Implementing Domain-Driven Design — the red book — translated Evans’ abstractions into concrete implementation guidance. Greg Young carried the model-centric view into CQRS and Event Sourcing. Alberto Brandolini turned the discovery half of DDD into a workshop format called Event Storming. What follows is a tour of the pieces they contributed, and of how the pieces hold each other up.

The ubiquitous language

Every DDD engagement begins with a language problem. The business has its own words — policy, claim, settlement, load, manifest, lane. Those words mean specific, precise things to the people who use them every day. Software projects almost always ignore this and invent a parallel vocabulary: Record, Item, Process, Handler, Manager. The translation between the two happens inside developers’ heads, it happens differently in each head, and it is wrong often enough that features drift away from what the business actually asked for.

The ubiquitous language is the discipline of refusing that translation. The team — developers, domain experts, product, QA — commits to a single vocabulary. The vocabulary appears in conversation, in whiteboard sketches, in user stories, in tests, in class names, in method names, in database columns. When a domain expert says “a claim is adjudicated when the adjuster signs off,” there is a method called adjudicate on a class called Claim, and no other operation in the system uses that word for anything else.

The language is not negotiated once. It is discovered continuously. Whenever a developer and a domain expert disagree about what a term means, or discover that a single word covers two situations that behave differently, the model has taught them something. Either the word needs to be split, or the model needs a new distinction. The model is the language, and the language is the model — that is the whole point.

The ubiquitous language is also bounded. It is ubiquitous within a context, not across the entire enterprise. A customer in the sales context is not the same thing as a customer in the fulfillment context, even if the CEO uses the word identically in both rooms. That observation is what forces the next pattern.

Bounded contexts

A bounded context is an explicit boundary — a piece of the system — inside which a particular model applies and a particular language is ubiquitous. Outside the boundary, the same word may mean something else, or the same concept may not exist at all. The bounded context is the unit of model consistency.

The failure mode without bounded contexts is the enterprise data model: the multi-year effort to define, once and for all, what a “customer” is across the entire company. These projects fail, reliably and expensively, because the premise is wrong. The sales team, the fulfillment team, the support team, and the finance team do not mean the same thing by “customer” — and they should not. Each of them has developed a model that fits their work. Forcing a single model onto all of them replaces four good local models with one bad global one.

DDD’s alternative is to accept that the enterprise is a patchwork of models, draw the boundaries explicitly, and manage the relationships between them. Inside a bounded context, the team is free to model with whatever precision their domain demands. At the boundary, they are responsible for translating to and from the languages of neighboring contexts.

Bounded contexts tend to correspond to subdomains, and to the teams that own them, and — in modern practice — to the services that deploy them. This is not a coincidence. When a service aligns with a bounded context, the service’s API is the boundary of the model. When it does not, the service is either trying to carry two models (and will be confused internally) or trying to share one model across two contexts (and will be coupled externally).

Evans distinguishes the core domain — the part of the business where the company actually competes, where bespoke modeling pays for itself — from supporting subdomains (necessary, but not differentiating) and generic subdomains (solved problems, buy don’t build). DDD’s full machinery is justified in the core; it is overkill almost everywhere else. Know which is which before you decide how hard to model.

The context map

Bounded contexts do not live alone. They exchange data, they call each other, they share users. The relationships between them are their own design problem, and Evans gave them names:

  • Shared Kernel. Two contexts agree to share a small, explicit subset of the model. Changes require coordination. Used sparingly; it is a promise that binds both teams.
  • Customer/Supplier. The downstream context depends on the upstream and has a voice in its roadmap. The upstream commits to not breaking the downstream.
  • Conformist. The downstream uses the upstream’s model as-is. Cheap, and appropriate when the upstream model fits — constraining when it does not.
  • Anticorruption Layer. The downstream refuses to let the upstream’s model leak in. A translation layer converts the upstream’s vocabulary into the downstream’s ubiquitous language at the boundary. This is the standard defense when integrating with a legacy system or a third-party API whose model you cannot change and do not want to inherit.
  • Open Host Service / Published Language. The upstream exposes a well-documented protocol meant to be consumed by many downstreams, in a shared format (today: JSON, protobuf, a formally published event schema). It is the reverse of conformist — the upstream accepts the cost of stability so the downstreams can be many.
  • Separate Ways. Two contexts choose not to integrate. Sometimes the cheapest integration is none.

The context map is the diagram of these relationships across the whole system. It is less a technical artifact than a political one: it shows which teams depend on which, where translation is happening, where friction is likely. Keep it current. A stale context map is how a system grows couplings nobody remembers approving.

Tactical design: the building blocks

Strategic design — bounded contexts, subdomains, the context map — is DDD’s answer to where to draw the lines. Tactical design is DDD’s answer to what the code inside a line looks like. Evans’ building blocks, still the standard vocabulary twenty years on:

Entities. Objects with an identity that persists across state changes. An Order with id ORD-419 is the same order tomorrow, with different contents, at a different status. Identity, not attributes, is what makes it the same thing. Two entities with all attributes equal but different ids are different objects.

Value Objects. Objects defined entirely by their attributes, with no identity of their own. Money(amount=100, currency=USD), Address(...), DateRange(...). Two value objects with the same attributes are interchangeable; you never need to ask “which one”. They should be immutable — a value object that changes is a different value object — and they are where a great deal of a domain’s precision lives. Money is not a decimal. A decimal cannot refuse to add dollars to euros.

Aggregates. A cluster of entities and value objects treated as a single unit for the purposes of consistency. One entity in the cluster is the aggregate root; it is the only member exposed to the outside world, and all access to the cluster goes through it. The aggregate’s boundary is also a consistency boundary: invariants that must hold atomically belong inside one aggregate.

Domain Events. Things that have happened in the domain that the domain cares about. OrderPlaced, PaymentCaptured, ShipmentDispatched. Events are immutable, past-tense, named in the ubiquitous language. They are how aggregates communicate changes to the rest of the system without being coupled to it.

Repositories. The illusion, for the domain code, of an in-memory collection of aggregates. orderRepository.findById(id) returns an Order aggregate; orderRepository.save(order) persists it. The repository abstracts the persistence mechanism so the domain code can be expressed in the ubiquitous language without knowing about tables, rows, or queries. One repository per aggregate type, not per entity.

Domain Services. Operations that do not naturally belong on any one entity or value object. A transfer between two accounts, a price calculation that depends on a rate table, a scheduling decision that depends on fleet state. These live in stateless domain services, named with domain verbs, not in managers or helpers.

Factories. The construction logic for a complex aggregate, when that logic is itself a piece of domain knowledge. A factory exists because how a thing comes into being is sometimes as much a domain concept as how it behaves afterward.

These patterns are useful, and they are also the piece of DDD most often misapplied. Naming folders after them is not doing DDD. Writing ClaimService, ClaimRepository, and ClaimEntity over the existing CRUD schema produces the same system, with more ceremony and worse names. The building blocks earn their keep only in service of a model the business recognizes.

The aggregate, in more depth

Vernon’s red book is clearest on aggregates, and the guidance deserves its own section because aggregate design is where most DDD implementations go wrong.

An aggregate is a consistency boundary. Everything inside it must be consistent at the end of every transaction. Nothing outside it is guaranteed to be consistent with it at the same moment — between aggregates, you accept eventual consistency, and you use domain events to propagate.

The rules that fall out of this:

  1. Protect true invariants in a consistency boundary. If the rule “an order’s total must equal the sum of its line items’ prices” must hold at all times, Order and LineItem are in the same aggregate. If a rule can tolerate being briefly out of date, the things it relates are in different aggregates.
  2. Design small aggregates. Large aggregates — “the customer owns every order they’ve ever placed, and each order owns every line, and each line owns every adjustment” — look tidy in a diagram and behave badly at runtime. Loading and saving a large aggregate for a small change is expensive and guarantees write contention. The cure is to break the aggregate up until each one corresponds to a single transactional unit the business actually cares about.
  3. Reference other aggregates by identity, not by pointer. An Order does not hold a Customer object. It holds a CustomerId. If the order needs customer data, it is retrieved through the customer’s repository, separately. This keeps each aggregate independently loadable and avoids accidental transactional coupling through an object graph.
  4. Modify one aggregate per transaction. The transaction is the boundary of strong consistency, and the aggregate is what fits in it. Cross-aggregate changes are expressed as a sequence of transactions connected by events — a saga, in the microservices vocabulary. A transaction that writes two aggregates is either a missed modeling opportunity (they should be one) or a missed consistency-relaxation opportunity (they are correctly two, and the rule between them can tolerate eventual consistency).

These rules are not about elegance. They are about what the system does under load. Small, invariant-protecting aggregates give you high throughput, low contention, and a clear story about what is consistent with what and when.

Domain events

A domain event records that something of business significance has happened. It is named in the past tense, in the ubiquitous language, and once it exists it does not change.

Events are how a single aggregate tells the rest of the world about a change without knowing who is listening. The order aggregate raises OrderPlaced; the warehouse context reacts by reserving stock; the billing context reacts by creating an invoice; the notifications context reacts by sending a confirmation email. The order aggregate does not know any of them exist. That ignorance is the architecture.

The shift from update-in-place thinking to event-first thinking is one of DDD’s most productive moves, and the one that connects DDD most directly to modern event-driven and microservice architectures. Once the team starts asking “what happened?” instead of “what is the current value?”, several things follow:

  • Integration between bounded contexts becomes a published event stream rather than a shared table.
  • Cross-aggregate workflows become sagas that react to events.
  • The audit log is not an afterthought bolted onto CRUD; it is the system of record.

That last point is what Greg Young pushed to its conclusion.

CQRS and Event Sourcing

Greg Young’s two big contributions — developed in the DDD community in the late 2000s and now reshaping event-driven architecture broadly — are CQRS and Event Sourcing. They are separate patterns, often confused, and they compose well.

CQRS — Command Query Responsibility Segregation. The observation is that the model required to handle commands (changes to state) is different from the model required to handle queries (reads of state). Commands need rich domain logic, invariants, aggregates. Queries need flat, denormalized, read-shaped data. Forcing one model to serve both compromises both.

CQRS takes them apart. The write side is a domain model: commands come in, aggregates enforce invariants, events go out. The read side is one or more projections — view databases built by subscribing to the events, shaped exactly for the queries the UI needs. The two sides can use different data stores, scale independently, and evolve separately. The cost is eventual consistency between them and the operational weight of running projections.

CQRS is not justified everywhere. Plenty of systems are small enough that a single model serves commands and queries adequately. It earns its keep when the read patterns and write patterns diverge enough that trying to serve both with one schema distorts the domain — typically in collaborative systems, reporting-heavy systems, and anywhere the write model is expensive to query.

Event Sourcing. Instead of storing the current state of an aggregate, store the sequence of events that produced it. The state is rebuilt by replaying the events. An Account aggregate is not a row with a balance column; it is a stream of AccountOpened, Deposited, Deposited, Withdrawn events, and the balance is the fold over that stream.

The benefits are real:

  • The audit log is the system of record. You cannot accidentally diverge from it because there is nothing else.
  • Temporal queries become trivial. The state as of any past moment is a replay up to that point.
  • New projections are cheap. A new read model is built by replaying history.
  • Bug fixes on the write model can be reapplied retroactively by replaying with the corrected logic.

The costs are real too:

  • Schema evolution of events is harder than schema migrations of tables, because old events stay in the log forever and your code must still be able to read them.
  • Projections must be rebuildable, which means they must be deterministic and versioned.
  • The mental model is unfamiliar to most teams and most tooling.

Event Sourcing composes naturally with CQRS — the event log is exactly the input the projections need — and with the domain-event vocabulary DDD already uses. But it is a commitment. Adopt it where the business cares intrinsically about the history (finance, audit, regulatory domains), and skip it where the history is merely convenient.

Event Storming

Alberto Brandolini’s Event Storming is the workshop format DDD needed for decades and finally got. It is the practical answer to “how do we actually discover the model together?”

The mechanics are simple enough to describe in a paragraph. You get the right people in a room — developers, domain experts, product, anyone whose knowledge the model depends on. You put a very long roll of paper on the wall. You hand everyone packs of orange sticky notes. You ask everyone to write down, in the past tense, every domain event they can think of — OrderPlaced, PaymentCaptured, RefundIssued, anything that happens that the business cares about — and stick them on the wall.

Then you arrange them on a timeline, left to right. Gaps appear, and the gaps are where the interesting conversations start. “Wait, what triggers this one?” “Who is allowed to do that?” “Is that two events or one?” Layer in other colors — blue for commands, yellow for aggregates, pink for external systems, purple for policies, red for hotspots where the group disagrees or doesn’t know. Within a few hours, the group has a shared picture of how the business actually flows, where the bounded contexts want to be, and where the current software model is lying about the domain.

Why it works is worth sitting with. Events are the unit of the workshop because events are things everyone in the room can observe and agree on. An argument about what an order is goes nowhere. An argument about whether OrderPlaced and PaymentAuthorized happen in the same moment or separately has a factual answer that the domain expert can give. Starting from events, the rest of the model — aggregates, contexts, policies — falls out naturally from the questions the timeline forces.

Event Storming comes in several flavors:

  • Big Picture — the whole business, no detail, a few hours, aimed at discovering bounded contexts and the big shape of the domain.
  • Process Level — a single business process in depth, aimed at understanding flow, actors, and policies.
  • Design Level — a focused session with developers in the room, aimed at producing the aggregates and commands that will be implemented.

Event Storming is not an alternative to coding. It is a way of making sure the coding is about the right thing. The orange stickies are not the deliverable. The shared understanding is the deliverable, and the stickies are the artifact that makes it visible.

What DDD will and will not buy you

DDD, when it fits, buys you a model that the business recognizes — which is to say, a system that stays explainable as the business changes, because the code changes in the same shape. It buys you bounded contexts, which let a large organization work on a large system without the whole company having to agree on every word. It buys you a vocabulary — entities, value objects, aggregates, events, contexts — that good designers have used for two decades to talk about precisely the problems large systems keep producing.

It does not buy you speed on small problems. A CRUD app with five screens does not become a better CRUD app because its folders are labeled with Evans’ vocabulary. It does not buy you the right architecture; you still have to pick bounded contexts well, draw aggregate boundaries well, and decide which events matter. The patterns are tools, not answers.

It especially does not buy you anything if the domain experts are not in the room. The entire discipline rests on a continuous conversation between people who know the business and people who build the system. No amount of *.Aggregate class naming substitutes for that conversation. If you cannot get the domain experts’ time, you cannot do DDD; you can only do architecture-flavored cosplay.

The working definition, after twenty-plus years of practice: DDD is what happens when a team commits to keeping the model in the code and the model in the business aligned, uses bounded contexts to keep that alignment local enough to be tractable, and uses tactical patterns to keep the code inside each context honest about what the business actually means. Evans named it. Vernon made it implementable. Young pushed the event-centric view into architecture. Brandolini gave us a way to discover the model in a room full of people who did not know, before they walked in, that they disagreed.

The work, in the end, is the same work it was in 2003. Talk to the people who know the domain. Write down the language they use. Draw the boundaries where the language changes meaning. Build the model inside each boundary out of things that behave like the domain. Let events carry the news between boundaries. Redraw when the business moves. That is the whole job.