Article ·

Domain-Driven Design: The Word the Code Got Wrong

What its terms mean, and where it pays for itself

Contents9 sections

One word doing two jobs

A domain expert said something in passing during a review. There are subjects, and history is one of them. There are also topics, and the Second World War is a topic. The expert wasn't correcting anybody. The distinction was so ordinary in his working life that it had never come up as a thing worth saying.

The code didn't have it. Knowledge sat under a single type called TopicWorker, and that type had been quietly doing both jobs since the day somebody wrote it. Two concepts were sharing one word inside the system, and they had never shared one in the conversation. The alternative to fixing it was cheap on any given day and ruinous over a year: every future discussion of that corner of the domain would have opened with somebody translating, one party saying topic and meaning subject, the other nodding and carrying the wrong thing away.

The repair was a rename to SubjectWorker, plus a real Subject type beside it. That move is the entire discipline. The language outside the code was already right, and the code was changed to match it rather than the other way round.

Domain-driven design, DDD from here on, carries one commitment: the model the code holds of the business and the model the business holds of itself have to be the same model. Aggregates, repositories, bounded contexts, and the rest of the toolkit all exist to keep that true while both models move. People arrive expecting a folder layout. They find a discipline, and a fair number of them leave again.

What the translation costs

Translation is the cost DDD attacks. An expert's meaning becomes a requirement; the requirement becomes a type name; and each hop drops something nobody notices until a rule changes and three teams disagree about what it used to say. The bug lives between the hops rather than inside any one of them. Nothing in the system represents the word the business actually uses, which leaves every hop free to invent its own.

Watch a sentence on its way in.

A policy lapses if the premium goes unpaid for thirty-one days, says the underwriter. An analyst writes that down as a requirement about flagging overdue accounts. A developer writes if (acct.statusFlag == 3). The word is gone. Lapse has left the building twice by then, and the artifact that ships to production carries no trace of the word that started the whole thing. A test named for the status flag passes cleanly while the rule it was written to protect has already gone wrong somewhere upstream, and nobody finds out until an underwriter reads a report and says that number can't be right.

Two translations, two losses, and the one vocabulary that refuses both.
Figure 1. Two translations, two losses, and the one vocabulary that refuses both.

A ubiquitous language refuses that whole sequence. It is one vocabulary, defined once and then used unchanged in the meeting and in the type name. Read policy.lapse(on=day31) aloud and most of the underwriter's sentence comes back.

Which domains are worth modelling

Ask whether a domain is worth modelling at all. Asked that way, the question sorts the work into three kinds, and the sort is a spending decision.

A core subdomain is why the business wins, and nobody can buy it. Model it properly, since this is where DDD earns its cost and close to the only place it does. A supporting subdomain is yours and necessary without being a differentiator, so build it simply; clean CRUD, meaning plain create, read, update and delete over a table, beats an aggregate more often than it loses to one. Generic subdomains look the same at every company that has one, so buy them.

What each kind of subdomain is worth spending on.
Figure 2. What each kind of subdomain is worth spending on.

Applying the full toolkit across all three is the commonest way DDD fails, and it fails expensively.

None of that ceremony shows up in a sprint review. All of it shows up two years later, when a change that should take an hour takes a week because it has to be threaded through four abstractions that were never carrying a rule. Aggregates get drawn around data with no invariant to protect. A domain layer gets laid over a vendor's identity service that already worked fine on its own.

Boundaries before patterns

Most teams reach for entities and repositories and skip the strategic half. Tactical patterns are cheap to change and boundaries are not, so the skipped half is the expensive one.

Everything strategic turns on one idea: a bounded context, meaning the region inside which a single definition of a term holds. Four teams at an insurer will each say Policy. Each of them means something different. Sales means a quote nobody has accepted. Underwriting means a risk being priced, carrying exposure and a decision. Claims means the contract in force on the date of loss, historic and never edited. Billing means a schedule of amounts due, and it cares about dates and money. None of the four is wrong. Reconciling them into one class is the mistake. A shared Policy serving all of them becomes a type nobody can change without breaking somebody else, accreting optional fields until it describes nothing precisely. A year in, it carries a nullable field for every context that ever needed one, a comment above each explaining which team put it there, and a test suite that stays green because every one of those fields is allowed to be null. Nobody owns it.

Four contexts, four meanings of Policy, and the named terms each pair integrates on.
Figure 3. Four contexts, four meanings of Policy, and the named terms each pair integrates on.

What you actually ship is a context map: which contexts exist, and on what terms each pair talks. Those terms are named patterns, and the names carry weight. Shared Kernel, Customer/Supplier, Conformist, Anti-Corruption Layer, Published Language, Separate Ways. Naming one commits both sides to how much of the other's model they are willing to absorb, which is a conversation two teams will otherwise have implicitly and badly, usually in a chat thread nobody can find afterwards.

The anti-corruption layer repays earliest and looks mundane going in. Our resident reads some forty external sources, and not one of those schemas reaches the graph; every source gets a small translator into our own typed entities instead. It costs a file per source. When a vendor renames a field or nests a payload one level deeper, the change lands in a single translator, gets fixed by whoever is on call that morning, and stops there instead of travelling into the entities the rest of the system reasons over. A foreign model changing shape becomes a local repair rather than a migration.

The vocabulary, defined

The tactical half of DDD is a short vocabulary, and most teams already half know it. Getting the definitions exact is what stops a repository from turning into a data access object with a fashionable name.

TermWhat it means
EntityHas an identity that persists through change. A policy is the same policy after its address is corrected.
Value objectDefined entirely by its attributes, with no identity of its own. Money, or a date range. Immutable, and compared by value.
AggregateEntities and value objects treated as one unit for change, with a single root as the only entry point.
RepositoryLoads and saves whole aggregates by their root. Never a data access object, and never one per table.
Domain serviceDomain behaviour that belongs to no single entity, such as a transfer between two accounts.
Domain eventSomething the business cares about having happened, named in the past tense: PolicyLapsed.
Application serviceThe thin outer layer: load, call a method, save. It orchestrates, and it holds no rules.

Table 1. The tactical vocabulary, with the distinctions that get lost first.

One entry carries more practical weight than the rest. A value object is defined entirely by its attributes and compared by value, which removes a whole class of bug and asks nothing of the surrounding architecture; Money and a date range are the standard examples. Both stay correct in a codebase that has adopted nothing else on the list, and that is the reason to start there.

Where the aggregate boundary goes

Aggregates are where DDD gets misapplied most often, because an aggregate looks like an object graph and behaves like a transaction.

Draw the boundary around the invariant, meaning the rule that has to be true at every instant. Total coverage may not exceed the sum insured. That has to hold continuously, so everything it spans has to commit together, and that is what puts Coverage inside Policy. Ownership doesn't put it there. Navigation doesn't either. Remove the rule and Coverage becomes its own aggregate.

The aggregate as a transactional boundary, sized by the invariant it protects.
Figure 4. The aggregate as a transactional boundary, sized by the invariant it protects.

Prefer small ones. A large aggregate turns into a lock-contention bottleneck the moment two writers want different parts of it, and a too-small one leaves an invariant with nowhere to live. Size follows from the rule, which reads as backwards to anyone who arrived expecting a modelling exercise.

Two things worth stealing

Neither of these asks you to adopt DDD wholesale, and both clarify a codebase out of proportion to what they cost.

Name domain events in the past tense. Ours read literature.book.studied and morality.action.scored. Past tense forces you to say what happened instead of what should happen next, and the difference shows up the first time somebody needs to listen without touching the publisher.

The second is a rule about transactions: one aggregate commits in one transaction, and never across two. That is a claim you can hold code to, and it fails loudly when something violates it. Our ingestion drain commits a batch as a unit, so a poison record can't leave the store half written.

What to avoid

Anaemic domain models are the classic failure: getters, setters, and every rule sitting in a service layer. That is procedural code wearing domain-shaped file names. If policy.lapse() doesn't exist, the language isn't in the code, whatever the folder structure suggests.

Buy the generic subdomains and mean it. Our resident models knowledge, affect and moral reasoning in detail, then reaches for off-the-shelf speech synthesis and transcription with no domain layer over either, because neither one is why the thing exists. Storage gets the same treatment. Event sourcing and CQRS, meaning command query responsibility segregation, sit in that same category: they compose well with DDD, they are independently expensive, and they should be adopted for their own reasons.

Start with language. Event-storm the domain by putting the events on a wall in time order, then let the boundaries and the vocabulary surface on their own. Model one context properly rather than four badly.

The test

Whether any of this worked has nothing to do with architecture. Ask whether a domain expert can read a test name aloud and agree with it. Ask whether a new rule lands in one place. Fail both and the diagram was decoration, however carefully it was drawn.

Somewhere in the code there is still a word doing two jobs. The expert has probably already said which one, in passing, and it went straight past.

Cloud / DevOpsART-CLD-009
Hashtags

About the author

Jason Newell leads organizations and people in technology, and is a researcher, deep technologist and builder — from a CRAY to agentic AI. He founded MAX Research Collective to build AI for regulated industries that runs where its owner has control, with healthcare as its strongest focus. About Jason → Connect on LinkedIn →

Free to read, share and quote: credit Jason Newell, MAX Research Collective, and link to this article’s original page, https://www.maxresearchcollective.com/articles/domain-driven-design.html. License & copyright →

Related

Related field notes

Keep reading

More field notes

This piece is part of the MAX Research Collective library. Browse the rest, or connect on LinkedIn.

Follow the collective

New research, projects and field notes as they publish. Following happens on LinkedIn — we never see an address, and there is nothing to unsubscribe from.

Or get it by email

An occasional summary of new research, each with a link to the full piece.