Domain-Driven Design: the half that matters
Eric Evans published Domain-Driven Design: Tackling Complexity in the Heart of Software in 2003. It is one of those books that a great many developers own, a smaller number have opened, and a smaller number still have finished. It is 560 pages, and the good stuff is not at the front.
What survives of it in most codebases is a folder layout. There will be a
Domain directory, an Entity directory, a Repository directory, and a
ValueObject directory, and inside them the same procedural code that would
have been there anyway. This is DDD adopted as a filing system. It costs
something and returns nothing.
The book has two halves, and they are not equally important.
Strategic design: the half that pays
The strategic half makes two claims. Both are about language rather than code.
The first is the ubiquitous language. The words used by the people who
understand the business and the words used in the source code should be the
same words. Not translated, not approximated — the same. When a claims handler
says lapse and the code says status = 3, there is a translation step, that
step lives in somebody’s head, and it is where defects breed. Every conversation
about the system pays a small tax, forever.
This sounds like a triviality and is not. Taking it seriously means that when the business changes a word, you rename the class. It means that a developer who cannot explain what a domain term means is not ready to write the code, and that a domain expert who cannot follow a description of what the code does has found a real problem rather than a communication failure. Most of the value of DDD is here, and it requires no framework, no folder structure, and no library.
The second is the bounded context. Any domain of a reasonable size will use the same word for genuinely different things. In sales a customer is a prospect with a pipeline stage. In fulfilment a customer is a delivery address and a set of access instructions. In billing it is a payment method and a liability.
The tempting move is to build one Customer — a canonical model, single source
of truth. It is nearly always a mistake. That class accretes every field any
part of the business ever needed, no team can change it safely, and it is
eventually maintained by nobody and feared by everybody. The DDD answer is that
these are three different models in three bounded contexts, each internally
consistent, each free to change, with the relationships between them made
explicit — a context map — and the translation at the boundary written down as
code rather than assumed.
That translation layer has a name worth knowing: the anti-corruption layer. When you integrate with a system whose model you do not control and would not have chosen — a legacy platform, a third-party API, an acquisition — you write an explicit layer whose job is to stop that model leaking into yours. It is tedious and it is almost always right.
Tactical design: the half that gets cargo-culted
The tactical patterns are the ones everyone can name. Entities have identity and a lifecycle. Value objects have no identity, are compared by their attributes, and should be immutable. Aggregates are clusters of objects with a single root, and the root is the only thing the outside world may hold a reference to. Repositories provide collection-like access to aggregates. Domain events record that something meaningful happened.
None of this is wrong. It is just downstream of the strategic work, and applying it without that work produces ceremony rather than design.
Where the tactical patterns do earn their keep is in a single idea: an aggregate is a consistency boundary, and it should be impossible to move it into an invalid state. Not “validated on the way in” — impossible.
final class Booking
{
private function __construct(
private readonly BookingId $id,
private DateRange $dates,
private BookingStatus $status,
) {
}
public function reschedule(DateRange $to, Clock $clock): void
{
if ($this->status !== BookingStatus::Confirmed) {
throw new BookingCannotBeRescheduled($this->id, $this->status);
}
if ($to->start() < $clock->now()) {
throw new CannotRescheduleIntoThePast($this->id, $to);
}
$this->dates = $to;
}
}
Two things are true of that class. There is no way to obtain a Booking that
was never confirmed and is somehow scheduled in the past, because there is no
setter through which to do it. And reschedule is a word the business actually
says — the rule about rescheduling lives in exactly one place, and that place is
findable by searching for the word people use when they talk about it.
The alternative — the one most codebases actually have — is a class of getters and setters with the rules living in a service somewhere above it:
$booking->setDates($to);
$booking->setStatus(BookingStatus::Confirmed);
$this->em->flush();
Martin Fowler named this the anemic domain model in 2003, and was clear that it is an anti-pattern rather than a simplification: you have paid for the mapping layer, the object graph and the ORM, and then put all the behaviour somewhere else. The objects are data structures with a bureaucracy attached.
The honest caveat is that anemic is not always wrong. It is wrong when there are rules. If the rule is “save what the user typed”, a domain model is overhead, and pretending otherwise is its own kind of dishonesty.
When not to do this
DDD is a way of paying down complexity in the domain. If the domain is not complex, there is nothing to pay down and the ceremony is pure cost.
A CRUD application — forms over data, few invariants, rules that amount to required fields — does not want aggregates, repositories and an application layer. It wants a well-organised set of controllers and a good ORM. Reaching for DDD there produces four files where one would do, and every one of them has to be maintained.
It is also worth separating DDD from the things it habitually arrives with. Hexagonal architecture (Cockburn’s ports and adapters), CQRS and event sourcing are frequently presented as a package, and they are not one. You can have a rich domain model without CQRS. You can have CQRS without event sourcing. Event sourcing in particular is a serious operational commitment — schema evolution, replay, snapshots — and adopting it because it appeared in the same conference talk is how teams end up maintaining infrastructure they never needed.
What it actually costs
The cost is not the patterns. It is the conversations. Ubiquitous language means sustained access to people who understand the business and are willing to argue about words; bounded contexts mean agreeing where the seams are, which is a political question as much as a technical one, because the seams tend to fall where the org chart does.
A team that adopts the tactical patterns and skips those conversations has taken on all of the cost and none of the benefit. That is the common failure, and it is why so many people conclude DDD is overengineering. They are describing something real. They are just describing the half that does not work on its own.
The underlying claim is more interesting than the patterns that carry it: that a system is intelligible in proportion to how honestly its language matches the language of the people whose work it does, and that when those two drift apart the software stops being a tool that its users can reason about and becomes one they can only submit to.
Further reading. Evans, Domain-Driven Design (2003) — the blue book; the strategic material begins around Part IV, and reading it first is defensible. Vaughn Vernon, Implementing Domain-Driven Design (2013) — longer, more practical, better on aggregate design. Vernon’s Domain-Driven Design Distilled (2016) is the short version if the other two are not going to happen.