When Domain-Driven Design Does Not Pay
The machinery costs vocabulary, indirection and mapping on every change. It repays only at real domain complexity, with real access to someone who knows the domain.
The requirement, the obvious build, and why it breaks
Every lesson starts where the work starts: someone asked for something, and the first implementation that comes to mind survives until the requirement changes.
What has to be true about my system and my team before this module's heavier ideas are worth their cost?
A new team lead proposes adopting DDD across a two-year-old product: aggregates, repositories, domain events, bounded contexts, the vocabulary. The product is a scheduling tool with six entities and a handful of rules, and there is no domain expert on the team.
These are the industry's best practices for domain complexity. Adopting them makes the codebase more maintainable, and doing it early is cheaper than retrofitting.
The techniques are a response to a specific problem — many interlocking rules, contested vocabulary, and invariants spanning objects. Applied where that problem is absent, they add cost with no corresponding benefit, and the cost is permanent (What an Abstraction Costs).
- The techniques are a response to a specific problem — many interlocking rules, contested vocabulary, and invariants spanning objects. Applied where that problem is absent, they add cost with no corresponding benefit, and the cost is permanent (What an Abstraction Costs).
- Without a domain expert, the model is invented by engineers. It will be internally consistent, confidently named, and describe a business nobody recognises — and it is much harder to fix a wrong model than to have had none.
- The vocabulary itself is a cost: aggregate, root, repository, bounded context, domain event. Every one of them is a term a new engineer must learn before they can add a field, and in a codebase with six entities they will not see why.
- While the rules are still changing weekly, a model encodes today's understanding into structure, and structure is the expensive thing to change. Modelling early is exactly backwards when the domain is not yet understood (Premature Abstraction).
- Partial adoption is the usual outcome and is worse than either endpoint: repositories without aggregates, aggregates without invariants, domain events used as an internal message bus. The names are present and the reasons are not (Speculative Generality).
What limits the solution, and what must never stop being true
This domain leads with these two. A design that ignores its constraints is not a design, and an invariant nobody named is one nothing is protecting.
- Four of the six engineers have never used these patterns, so adoption includes a learning period during which the code will be wrong in confident-looking ways.
- The nearest thing to a domain expert is a customer success manager who has twenty minutes a week.
- The product is still finding its market, so the rules themselves change faster than the code that implements them.
- Whatever is adopted, a new engineer must be able to find where a rule is enforced without asking anybody.
- The cost of adopting a technique must be paid by a benefit someone can name in advance.
Who owns what, and where the seams fall
Responsibilities decide boundaries; boundaries decide what an interface has to say.
- Whoever proposes the adoption owns naming the specific problem it solves in this system, with an example of a change that is currently expensive.
- Whoever approves it owns the counterfactual: what happens if nothing changes and the system grows for another year.
- The team owns choosing pieces separately. Value objects, aggregates, ubiquitous language and bounded contexts are four decisions, not one (Reversible and Irreversible Decisions).
- The useful boundary here is between the tactical patterns — value objects, entities, aggregates — and the strategic ones — bounded contexts, context maps. The tactical ones can be adopted piecemeal in a single module; the strategic ones are organisational and are the expensive half.
- A pattern adopted in one bounded area with a clear before and after is a reversible experiment. Adopted repository-wide by convention, it is not (Incremental Migration).
What has to be true before it pays
Three conditions, and the argument of this lesson is that all three are required rather than any one. A complex domain without an expert produces a confident fiction; an expert and a simple domain produces ceremony; both with a system that will be replaced in eighteen months produces cost with no time to amortise.
Check them honestly before adopting, and check them again in a year, because two of the three can change.
| Trigger | Symptom | Cause | Response |
|---|---|---|---|
| Complex rules, no domain expert available | A confident model with names nobody in the business uses; every requirement needs translating into it. | The model was invented rather than learned, so it encodes engineers' assumptions as structure. | Get expert access first, even twenty minutes a week, and model one area with them. Without that, keep the rules as named functions where they are cheap to be wrong (Ubiquitous Language). |
| Expert available, but only a handful of simple rules | Aggregates with no invariants, repositories wrapping one table, a mapper per entity. | The machinery was adopted for its own sake; there was no problem for it to solve. | Take the value objects and the naming and stop there. They deliver most of the daily benefit at almost no cost (Value Objects). |
| Both present, but the product is pre-market-fit | The model is restructured every few weeks and each restructure is expensive. | Structure encodes understanding, and the understanding is still changing weekly. | Defer. Keep rules as functions that are cheap to move, and revisit when the rules stop changing faster than the code (Design for the Known, Name What You Assumed). |
| All three present, adopted repository-wide at once | Four engineers writing patterns they do not yet understand; reviews about vocabulary rather than behaviour. | Adoption was a rollout rather than an experiment, so nothing was learned before everything was committed. | Pilot one module, measure a real change against an unpiloted module, then decide (Incremental Migration). |
Take it apart: four decisions, not one
The most useful thing in this lesson is that "adopting DDD" is not a single choice. The pieces have wildly different costs and wildly different benefits, and the two cheapest ones deliver a large share of the value.
Reading down the cost column tells the story: the first two are affordable in any codebase, the third requires a real invariant, and the fourth is an organisational decision that has very little to do with code.
For each technique separately: what problem does it solve in this system, and what does it cost every change from now on?
when Always worth considering. Costs a rename and buys greppability for requirements phrased in business language.
cost A large diff once, plus keeping dashboards and runbooks in step. No ongoing indirection at all (Naming and Domain Language).
when Whenever values have units, validity rules or operations — which is most systems that handle money, dates, quantities or identifiers.
cost A dozen small types, some mapping code, occasional framework friction. The best ratio in the module and adoptable alone (Value Objects).
when Only where you can state an invariant spanning objects and point at the path that violates it today.
cost Whole-boundary loads, write contention, a mapping layer, and a vocabulary the team must learn. Real and permanent (Aggregates).
when When separate parts of the business genuinely mean different things by the same word, and separate teams own them.
cost An organisational commitment with translation layers between contexts. This is architecture, not code structure, and it is the piece least often needed by the teams that adopt it (Anti-Corruption Layer).
when Never a reason.
cost The full tax with no named benefit, and a half-adopted result that is harder to reason about than either endpoint (Pattern Overuse).
The closing position of this module
Everything in the preceding nine lessons is a tool with a price. Value objects are nearly free and nearly always worth it. Entities cost a little discipline and repay it in any system with things that persist. Aggregates cost contention and repay only where a real invariant spans objects. Domain services are a small useful category with a strong tendency to become a dumping ground. An anemic model is the right answer for a great many systems and the wrong one for a few, and knowing which you are in requires counting rules rather than quoting principles.
The failure this module most wants to prevent is not under-modelling. It is a team adopting the whole vocabulary because the literature is confident, discovering in year two that the machinery does not fit, and being unable to remove it because everything now speaks through it. Modelling is a bet like any other structural decision, and the way to make it well is to name the change it makes cheap and check whether that change is actually coming (The Cost of Change).
- Almost always worth it: domain-aligned names, value objects, one home per rule.
- Worth it when you can name the invariant: entities with real identity, aggregates, aggregate roots.
- Worth it in a narrow band: domain services, kept stateless and named after operations.
- Legitimate and widely maligned: anemic models and transaction scripts, for systems whose rules are few or independent.
- Requires the organisation, not just the code: bounded contexts, context maps, domain events as an integration mechanism.
- Never a reason: "it is best practice", "the book says so", or "we might need it later" (Speculative Generality).
How to build it
Most important first.
- Take the cheap pieces first and separately. Value objects and domain-aligned naming cost almost nothing, work in any codebase, and deliver most of the everyday benefit (Value Objects, Naming and Domain Language).
- Adopt aggregates only where you can state the invariant and name the code path that currently violates it. No invariant, no aggregate (Aggregates).
- Require domain-expert access before investing in a model. Without someone who can say "no, a reservation can outlive the order", the model is fiction, and fiction with confident naming is the worst artefact this module can produce.
- Prefer one module over the whole repository. Do it where the rules actually are, leave the CRUD as CRUD, and compare after a quarter (Transaction Script).
- Write down what would make you stop, and check it. "If a new engineer still cannot find where the cancellation rule lives after this, we have not solved the problem we named" (Revisit Triggers).
What the next change costs
The field this whole domain exists for. A structure is only better if it makes the change after this one cheaper — and it is worth saying which changes it does not help.
- Under full adoption in a simple domain, adding a field costs a model change, a mapper change, a persistence change and a DTO change. Four edits where there was one, forever, on every field.
- Under selective adoption — value objects and good names, no aggregates or repositories — adding a field costs one edit and adding a rule costs one, which is the best of both for a system with few invariants.
- The change that full adoption genuinely makes cheap, and it is worth naming honestly: a rule change in a domain with many interlocking invariants. One method, no discovery, and no path can skip it. If your system has that problem, the tax is worth paying.
- The change that becomes catastrophic: undoing it. Removing an inappropriate model requires touching every module that learned to speak through it, which is why this decision deserves more scrutiny than its reversibility suggests.
- The recommendation here — take the cheap pieces, defer the rest — costs consistency. A codebase where some modules are modelled and others are scripts requires judgement to navigate, and some teams genuinely function better with one uniform style.
- Deferring the heavy patterns means occasionally retrofitting them under pressure, which is more expensive than having built them, and that cost is real rather than rhetorical.
- Refusing to adopt a vocabulary the wider industry uses has a hiring and communication cost that is small but not zero.
What can go wrong
- Cargo-cult adoption: the names appear, the reasons do not, and the codebase acquires a
domain/folder containing anemic classes and a repository per table (Decomposition by Folder). - Model invented without expertise, encoded into aggregates, and then defended because rewriting it is expensive — a wrong model is stickier than no model.
- The vocabulary becomes a barrier to contribution. Engineers who do not know the terms stop proposing changes, which is a knowledge-sharing failure produced by a design decision (Knowledge Sharing).
- The mitigation fails too: a pilot in one module is declared a success on the strength of enthusiasm rather than a measured change cost, and rolled out everywhere — so the experiment produced no information (Decision Records).
- Adoption depends on people: a domain expert, and enough of the team understanding the patterns to review each other. Both are dependencies with lead times and neither is a code change.
- Every layer added — repository, mapper, application service — is a dependency each future change must traverse, which is where the "indirection tax" is actually paid (Change Amplification).
- Framework and ORM choices constrain how expensive the tactical patterns are, so the decision depends on the stack more than the literature suggests.
- "So DDD is bad." It is not. Where the domain is genuinely complex, an expert is available, and the system will live for years, it is the best-developed answer we have. The claim here is narrow: those conditions are much rarer than adoption rates suggest.
- "We can do DDD-lite." Sometimes yes — the tactical patterns are separable and value objects alone are worth it. But "lite" often means the names without the invariants, which buys nothing and costs the vocabulary (Pattern Overuse).
- "Our domain is complex." Almost every team believes this. The falsifiable version: how many rules interact with each other, and how many places can violate any one of them? Count, then decide (Essential and Accidental Complexity).
- "Start simple and refactor later." A reasonable default, but not free — retrofitting an aggregate into a system with twenty write paths is real work, and teams that plan for it often do not do it. Name the trigger and mean it (What Technical Debt Actually Is).
- speculative-generality
- utility-dumping-ground
Testing it, and how it ages
- The honest test of adoption is a change-cost comparison, not a code review: take a real requirement, implement it in the piloted module and in an unpiloted one, and compare what each touched (Change Amplification).
- Test comprehension, not just code: ask an engineer who was not involved to find where a named rule is enforced. If they cannot, the structure has not delivered the benefit it was adopted for.
- Keep the pilot's tests independent of the patterns, so the module can be simplified back without rewriting its test suite (What a Unit Is).
- Systems genuinely do grow into these patterns, and the growth is usually driven by one or two areas rather than the whole codebase. A mature system with a modelled billing core and CRUD everywhere else is a good outcome, not an inconsistent one.
- The patterns age well where the invariant they protect stays real and age badly where the rule moved elsewhere — into a rules engine, a partner's system, a regulation that is now checked externally.
- Vocabulary outlives its reasons. Five years on, a codebase often contains aggregates whose invariants were removed, and nobody dares simplify them because the names sound authoritative.
Where this applies
This domain's advice is contested more than most. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view rather than a caricature.
- CONTESTEDThe strongest opposing view, stated fairly: teams systematically underestimate how complex their domain will become, and the patterns are far cheaper to adopt at year one than at year four — by which point twenty write paths, three teams and a public API all depend on the current shape. On this view the "wait for evidence" advice is survivorship bias, because the systems that waited and suffered are not the ones writing blog posts. This is a serious argument made by experienced people, and the honest resolution is that it is right about long-lived systems in rule-heavy domains and wrong about the median product, which is retired or rewritten before the investment matures.
- DOMAIN-SPECIFICThe break-even sits at interacting-rule count, not system size. Insurance, tax, payroll, trading and logistics cross it early; scheduling tools, CMSs, internal admin, integrations and analytics generally never do, no matter how many users or tables they accumulate.
- LIFETIME-SPECIFICA system with a known three-year life and a stable team can carry manual consistency indefinitely. One expected to run for a decade across several team generations cannot, because the argument for scripts depends on people remembering where the rules are — and that assumption expires with the people.
- SCALE-SPECIFICBelow roughly ten engineers, shared understanding substitutes for structure and the patterns mostly cost. Above that, structure is the only mechanism that survives turnover, and the same patterns start looking cheap.
Where the depth lives
This domain teaches the codebase-level structure and hands the rest off.
- — System Design — bounded contexts and service boundaries are frequently conflated; deciding where a network boundary goes is a scale and ownership question that a domain model informs but does not settle.
- — Testing & Reliability Engineering — the most honest evidence for or against adoption is a measured change-cost comparison between a piloted and an unpiloted module, which is an experiment design question more than a modelling one.