← All articles

DDD's Best Ideas Are the Ones Nobody Copied

Ask a team that does DDD what they have. Usually the answer is Entities, Repositories and Services — the tactical half, copied as a coding style. The strategic half, where the value actually is, gets skipped. The result has a name, a predictable failure mode, and a fix that starts with language rather than base classes.

"We do DDD."

One question sorts out what that means: do you have bounded contexts and a context map, or do you have Entities and Repositories?

It is almost always the second. And the second, on its own, has a name and a reliable outcome. It is called DDD-Lite, and the verdict it earns is not a subtle one:

DDD-Lite produces inferior domain models.

Not suboptimal. Inferior — worse than what the same team would have built with no framing at all, because the vocabulary supplies the confidence of a method without any of its discipline.

Two halves, very different fates

Domain-driven design ships two distinguishable sets of ideas.

The tactical half is the building blocks: Entity, Value Object, Aggregate, Repository, Domain Service, Factory. Concrete, code-shaped, teachable in an afternoon.

The strategic half is boundaries and meaning: Bounded Context, Context Map, Ubiquitous Language, and distillation — the discipline of identifying which part of your domain is actually the core and spending your best people there.

The industry copied the first half almost universally and the second half almost not at all. That is backwards, and the imbalance explains most of the disappointment.

Why the tactical half got copied

Not because it is more valuable. Because it is more copyable.

It looks like OO scaffolding you already recognise. It maps onto folders. Frameworks ship a Repository base class, so adoption is npm install. It is concrete: you can tell whether a class is an Entity, and you can tell on Friday whether you did it.

The strategic half offers none of that comfort. "Draw your context boundaries" is not a library. It requires talking to domain experts, disagreeing about what a word means, and making an org-shaped decision that no framework will validate for you. It cannot be done in an afternoon and cannot be verified by a linter.

So teams adopt what they can install, and report that they have adopted DDD.

What you get instead

The predictable result is the anemic domain model: objects carrying data — fields, getters, setters — with the actual business logic pushed out into service classes that manipulate them from outside.

You end up with procedural code wearing an OO costume. The whole point of putting rules next to the data they govern is gone. Invariants cannot be protected, because any caller can set any field. Logic scatters across services and duplicates, because there is no single place it belongs.

// The shape of DDD, none of the substance.
class Order {
  id!: string;
  status!: string;
  lines!: OrderLine[];
  total!: number;          // maintained by... whoever remembers
}

class OrderService {
  addLine(order: Order, line: OrderLine) {
    order.lines.push(line);
    order.total += line.price;   // and every other service that touches lines
  }                              // must remember to do this too
}

It has Entities. It has a Repository. It has a Service. It is a transaction script with extra files, and nothing in the tactical vocabulary told the team otherwise.

Where the value actually was

The strategic patterns are the ones that keep paying:

Bounded Context is the idea that a model is only coherent within a boundary, and the same word legitimately means different things outside it. This is the single most load-bearing idea in the whole approach — and, years later, it is what the industry rediscovered when it needed a principled way to decide where one service ends and the next begins. Teams that draw service boundaries by team org chart or by noun are re-deriving this badly.

Context Map makes the relationships between those boundaries explicit — who is upstream, where translation happens, which team's model wins. Left implicit, those relationships still exist; they are just invisible until an integration surprises you.

Ubiquitous Language is where model quality actually comes from. If the domain expert's word is not the class name, you are maintaining a translation layer in people's heads, and that is where precision leaks.

Distillation — deciding what is core domain versus supporting versus generic — is the one that determines where your best engineers spend the year. Almost nobody does it deliberately.

Two decades on, the scoreboard is clear: the strategic ideas have aged best, and the tactical patterns pay off only when applied with rigour rather than adopted as a coding style.

This is not "tactical bad"

The correction is not to throw out Aggregates. Applied with rigour, the tactical half is exactly what prevents anemia, and the cure is specific. Four rules of aggregate design do most of the work:

  1. Model true invariants in consistency boundaries — an aggregate exists to protect a real business rule, transactionally. Not to mirror a table.
  2. Design small aggregates — the single most common design mistake is aggregates that are too big.
  3. Reference other aggregates by identity, not by object reference.
  4. Use eventual consistency outside the boundary — one transaction modifies one aggregate.

Add "Tell, Don't Ask" and no public setters, and the anemic model becomes structurally hard to write. That is the tactical half doing its job.

So the failure is not tactical-versus-strategic. It is tactical without strategic, and without rigour — patterns adopted as scaffolding, with the thinking left out.

The honest counter

DDD targets complex domains. If your system is genuinely CRUD — forms over data, few invariants, rules that fit in a paragraph — then even the strategic ceremony is overhead. Bounded contexts for a six-table admin tool is a cost with no matching benefit.

The test is not the size of the codebase. It is whether the domain has rules that are hard to get right and expensive to get wrong. If it does not, skip all of it; you will not be missing anything.

Re-prioritise

If you are starting, or restarting:

  1. Get the language right first. One term, one meaning, in speech and code. Cheapest, highest leverage, no framework required.
  2. Draw the boundaries. Where does a word change meaning? That is a context edge, and it is telling you where a real seam in your system is.
  3. Map the relationships. For each seam, name who is upstream and where translation happens.
  4. Then reach for aggregates — with the four rules, not as folder names.

Most teams run that list in exactly reverse order, starting at step four because it is the one that comes in a package.

The takeaway

Next time someone says they do DDD, the useful question is not which patterns they use. It is:

What are your bounded contexts, and where does the language change?

If there is a crisp answer, the tactical patterns are probably doing real work. If the answer is a list of base classes, the team has bought the half that was easy to copy — and you already know where that ends.