On October 17, I presented on Part I of my DDD Effective Aggregate Design essay at the Denver-Boulder DDD Meetup. We had a nice attendance and once again benefited from the use of Quark’s conference room. We had a good turn out with around 20 in attendance.
Even after the essay and presentation, I am concerned that the vital message about true invariants is being missed by some. Of course it is to be expected since anyone who has designed aggregates has faced the various challenges of grasping true business rules that absolutely require transactional consistency.
Still, a recent conversation indicates that modelers many times take the opposite direction found in Part I of my essay, guarding various domain object life cycles by placing them inside an aggregate. True, sometimes doing this represents an actual business rule. In that case domain experts would explicitly insist that ‘such-and-such must not be removed from the system until thus-and-so is also removed.’ That may speak to a true consistency rule. However, many times the management of a given entity life cycle isn’t a true business rule, and modeling with that in mind causes unnecessary aggregate bloat and related negative consequences.
So, what are some effective ways of managing life cycles of separate aggregates without incorrectly modeling their boundaries? Consider these approaches:
1. Use code reviews. This may seem far too obvious to state, but it is one of the simplest ways to ensure that when one domain object depends on the existence of others, looking out for untimely removal by employing peer review is an effective first step. Always look for code like this:
someRepository.remove(aggregateInstance);
When you find it, ask yourself if this is appropriate and permissible given that the possibility exists that other aggregate instances require its existence.
2. Implement repository remove() operations to refuse inappropriate removal. Using an example from my essay, consider the possible inappropriate removal of BacklogItems and Sprints. If a BacklogItem is committed to a Sprint, it must not be removed. Otherwise, the Sprint’s integrity would be compromised by it referencing a now non-existing, yet committed, BacklogItem. Too, if we removed a Sprint, any BacklogItems committed to it would then wrongly reference the now defunct Sprint. We could protect against the inappropriate removal of either of these aggregates without trying to place a convoluted consistency boundary around them. In a repository’s remove() operation implementation, we can check each aggregate instance for a state that would break dependencies if the operation’s normal completion was carried out. For example, the BacklogItemRepository would check whether each BacklogItem is committed to a Sprint. If so, the remove() operation would throw an IllegalStateException. The same goes for the Sprint. If its repository discovers that it has any BacklogItems committed, it would refuse to remove the Sprint instance, throwing an exception.
3. Never perform physical aggregate instance removal. Under some circumstances, even the above two measures could be insufficient. Or even if removal was appropriate at one time, new dependencies could develop over time as new requirements are established. Of course, if the new requirements call for a true dependency invariant, we’d move an existing aggregate inside the boundary of another, forming a new aggregate definition. However, if it’s not a true invariant, consider using a status to reflect the current logical state of a removed instance: REMOVED. That way domain aggregates never completely disappear from the system, they just reach a state where they cannot be consumed using repository queries. If need be, you could change the status back to EXISTING in order to avert disaster. Or even if the instance remains in the REMOVED state, you can still view the instance to see when, why, and by whom it was removed, and even analyze its attributes.
This is, in fact, how BacklogItems are modeled in my sample core domain. We actually never want to delete any BacklogItem from the persistence store, but we can transition it to a state that causes it to be filtered out of normal view queries.
4. Similar to #3, if you are using an event store you can establish that an aggregate instance has been removed, but you would never lose the history of state transitions for the formerly existing domain object. With added advantage, the event store would allow you to reconstruct the state of the aggregate instance at any point in time.
While this list may not be exhaustive, it does show that there are alternatives to modeling chunky aggregates to preserve dependencies when they don’t reflect true business rules. Yet, the real challenge is in recognizing when a true invariant exists and when it does not. Distinguishing the difference will allow us to design small aggregates that reflect true business rules, and at the same time keep our systems performing and scaling optimally.