Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
So, you are applying domain-driven design and want to define aggregates. Let’s go over ways to define them with an example domain, and you might be surprised by the outcome. Domain modeling is about capturing domain concepts not just as data and relationships but exposing behaviors.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
This is a continuation of Part 1, where I reviewed a common domain modeling approach, which I disagreed with and explained why.
The TLDR is you cannot start domain modeling by defining entities, value objects, and their relationships. Ultimately, you need to define what your system actually does in terms of the capabilities and behaviors it provides.
The general gist of the example was a dinner-hosting system with a defined set of entities.
The issue outlined in Part 1 is that the modeling approach used was to define entities and then see what would be the root, and all other references in other aggregates would be value objects just representing the entity’s ID. As an example:
As mentioned in Part 1, this defined a database schema, not an aggregate.
An aggregate is a consistency boundary and enforces business rules. What the business rules are and how they are enforced depends on the behaviors. You invoke a behavior, which then has to enforce the business rules. And the business rules then rely on the state/data.
Suppose you think about a dinner in the system. What’s the workflow of that? Most complexity lives in workflows and the lifecycle of entities or aggregates.
A dinner goes through a lifecycle. It has a start, goes through a series of transitions, and then ends. It’s not just a series of entities and relationships. All these events are actually what happens in the dinner.
But how did all these events happen? Well, they occurred when various commands occurred. These are the capabilities we are exposing. You can Plan a Dinner, Reserve a Seat, Cancel a Reservation, etc.
To be clear, I’m making up these behaviors in this made-up example for illustration purposes. But the point is your behaviors are going to drive your design.
Reserving a seat had some invariants that needed to be enforced. Maybe we have a limited number of seats at a dinner.
But do we even need an aggregate here? We can have this workflow and lifecycle progression of a dinner, but do we need to model this using an aggregate?
An aggregate is a consistency boundary. Meaning we have a collection of related objects that need to be consistent. We invoke state changes through the root so it can coordinate that consistency.
In my example, it’s just the reservation and seating limit. While we can use an aggregate to enforce the rule and consistency, we don’t need to for one use-case. We could just use a transaction script. I’m not against an aggregate here as it defines all the behaviors and data behind it, but is it overkill?
It could be, and we could use a transaction script instead. A transaction script is just a procedural set of operations all wrapped in a transaction (for consistency) of all your state changes for an individual request.
You could create a transaction script for all the above commands outlined instead of having them all defined in an aggregate.
Transaction scripts can start to go sideways, however, when you start adding more business logic or needing to share logic between transaction scripts. That’s when you’ve gone too far, and an aggregate would likely be better served.
But regardless of an aggregate or a transaction script, you’re defining either based on the capabilities and behaviors of the system. They drive you, not data or entities.
Let CRUD be CRUD
But not everything has a workflow or a lifecycle. Take, for example, a menu. If I’m making up what that might look like, here’s a series of things that might happen to a menu.
That’s just CRUD. We create a menu, update it, add a new time, update an item. Only reviewing a menu in this list sounds interesting and likely has some behaviors around it.
Not everything is CRUD, and not everything has rich behaviors around it.
Behaviors determine the data you’re encapsulating. When you realize the behaviors you’re exposing, the business rules around invoking those behaviors, and finally, the data behind that, then you’ll have a better idea of how you might model that.
The best choice might be an aggregate. Or, instead, it might be a transaction script. Domain modeling isn’t a technical one-size-fits-all implementation.
One model doesn’t rule them all. You may have a concept in your system that has various representations. A guest in the context of a dinner might be very different from a guest in the context of marketing. You don’t need one singular model to solve all problems.
One last key point to make is that the concerns around reads often differ from writes. If you’re using an aggregate because it’s a consistency boundary, that means you’re changing state. That’s a write. The data you need within the aggregate for consistency might differ from the data you want when you perform various queries. If that’s the case, an aggregate wouldn’t likely be used for queries. This is why CQRS is talked about.
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.