Posts in this series:
- A primer
- Aggregate Construction
- Encapsulated Collections
- Encapsulated Operations
- Double Dispatch Pattern
- Avoiding Setters
- Domain Events
Recently, I talked some about the idea of an intentionally anemic domain model, under the name of “Persistence Model”. While a Persistence Model is great for a large percentage of projects, you may eventually want to move more behavior into the domain. That doesn’t mean a bevy of domain services doing the actual work, however. A strong domain means that our objects become more behavioral, and less as solely data-holders.
But before we get into some of the patterns, what are some of the goals we want to achieve with a stronger domain? And how do we get there, what should we be looking for?
A lot of DDD is just plain good OO programming. Unit tests and code smells are the best indication that our domain is wrong, along with conversations with our domain experts. In systems that start to move beyond merely CRUD, specific code smells start to surface that should indicate that our system is starting to accumulate behavior, but it just might not be in the right place. In a behavior-rich, but anemic domain, the domain is surrounded by a multitude of services that do the actual work, and fiddle with state on our domain objects. The domain objects contain state to be persisted, but it’s not the domain objects themselves exposing any operations.
But this is just code smell! Lots of code smells indicate that our domain is not as rich as at could be. The behavior is out there, but just needs to be moved around. Some smells I look for include:
- Primitive Obsession
- Data Class
- Inappropriate Intimacy
- Lazy Class
- Feature Envy
- Middle Man
All these are smells between classes, where usually a domain service is waaaay to concerned with a set of entities, when behavior could just be moved down into those entities
One of the most quoted, but most misunderstood ideas in DDD is the concept of aggregate roots. But what is this aggregate root? Is it just an entity with a screen in front of it? A row in a database? Evans defined a set of rules for Aggregates:
- The root Entity has global identity and is ultimately responsible for checking invariants
- Root Entities have global identity. Entities inside the boundary have local identity, unique only within the Aggregate.
- Nothing outside the Aggregate boundary can hold a reference to anything inside, except to the root Entity. The root Entity can hand references to the internal Entities to other objects, but they can only use them transiently (within a single method or block).
- Only Aggregate Roots can be obtained directly with database queries. Everything else must be done through traversal.
- Objects within the Aggregate can hold references to other Aggregate roots.
- A delete operation must remove everything within the Aggregate boundary all at once
- When a change to any object within the Aggregate boundary is committed, all invariants of the whole Aggregate must be satisfied.
It’s a lot, but I generally think of Aggregate roots as consistency boundaries. When I interact with an Aggregate, its invariants must always be satisfied. Keeping invariants satisfied strengthens the design and responsibility of the objects, as the logic that defines the Aggregate is then self-contained.
We may not like it, but not all of our system’s model will ever reach our vision of perfection. Which is fine, as a perfect model across our entire system would be prohibitively expensive, with little ROI on all that work. Instead, we have to focus on specific areas of the core domain that provide the most value to the customer. The better our core domain model, the better our system represents the conceptual model we’ve defined with our customers, and the better we will be able to serve their needs.
Unfortunately, all the interesting Strategic Design chapters are after the basic DDD patterns in the book, so that many folks get lost discussing repositories, entities, aggregates and so on. But in recent interviews, Evans mentions that these later discussions are more important than the earlier ones. The bottom line is that we have to choose carefully where we spend our time refining our domain model. Some areas will not be as refined as others, but that’s perfectly acceptable. The trick is to find which areas will give us the most value for our time spent in refactoring, refinement and modeling.
Finally, it’s worth noting that there is no “best” design. There is only the “best design given our current understanding”. Software development is a process of discovery, where concepts that seemed unimportant or hidden may suddenly become obvious and critical later. That’s still normal, and not a negative thing. Lots of concepts around an idea will be thrown around, but only the most important ones will rise to the top, and sometimes that just takes time.
So you’ve decided to do DDD, but all you’ve done is establish good names for objects that sit on top of database tables. Not really an improvement, and probably a lot of work. You look at some DDD sample applications, but they all seem overly complex for what seems like over-engineered CRUD applications. The last true DDD system I handed off to another team, they would have been at a loss if I just gave them software to look at. Instead, software diagrams, code, whiteboarding, and a lot of conversation about the core domain were what allowed us to have a successful handoff. With sample applications built on things like the Northwind or AdventureWorks database, you won’t see that critical tipping point of domain complexity that makes DDD required for long-term success.
In the next few posts, I’ll highlight some common DDD patterns that can help move your core domain model from one of data-driven, anemic models, to rich, behavioral models.