Skip to content

How to Model Large Scale Business Rules in DDD with Aggregates

In Domain-Driven Design (DDD), aggregates are a cornerstone for maintaining business rules and ensuring consistency. However, a common challenge arises when an aggregate seemingly needs to manage a vast collection of related entities. This can lead to significant performance bottlenecks and memory issues.

A frequent question that illustrates this problem perfectly is: “How do I handle an aggregate for something like a chat group that could have millions of messages or members, or a user with millions of friends?” The core issue is enforcing a rule like, “A group chat cannot have more than 100,000 members.”

The intuitive approach is often a trap. Let’s explore this pitfall and then look at four alternative, more scalable solutions.

The Common Pitfall: Modeling Relationships, Not Rules

The most direct way to model this seems to be creating a GroupChat aggregate that holds a list of all its Member objects. To enforce the rule, you’d simply check the size of the member list before adding a new one.

While this feels logical, it’s a trap. To perform this seemingly simple validation, you would need to load the entire collection of up to 100,000 user objects from the database into memory. This is incredibly inefficient and will not scale.

The fundamental mistake here is that we are modeling the relationship (the collection of members) rather than the behavior or the rule we actually need to enforce.

Alternative 1: The Lean Aggregate - Focus on the Count

A much more efficient approach is to realize that to enforce the rule, you don’t need to know who the members are, just how many there are.

You can modify the GroupChat aggregate to simply hold a memberCount property. When you add a new member, the aggregate’s only responsibility is to check this count against the maximum limit and then increment it. This is lightning-fast and avoids loading a large object graph into memory.

  • Trade-off: The main trade-off is that you now need to manage the actual association between users and groups separately. This approach keeps your aggregate lean but requires another mechanism to track the membership list itself.

Alternative 2: The Application Layer - Shifting Responsibility

If you’re uncomfortable with the potential for the memberCount to get out of sync with the actual list of members, you can move the enforcement of this rule up a layer to your application service.

In this scenario, your application layer would first perform a fast query on the database to get the current count of members for the group. If the count is below the limit, it would then proceed to add the new user.

  • Trade-off: This method keeps your aggregate from being responsible for this specific invariant, but it also means your business logic is now scattered between the domain and application layers. This can be a valid approach, but you must accept that the application layer is doing more than just passing data through; it’s performing orchestration and validation.

Alternative 3: The Pragmatic Choice - A Simple Transaction Script

Sometimes, you don’t need an aggregate at all. For a straightforward rule like this, a simple transaction script can be the most effective solution.

You can start a database transaction, set the appropriate isolation level, and run a COUNT query on your members table. If the number is within the limit, you perform the INSERT for the new member and then commit the transaction. This is simple, fast, and reliable.

  • Trade-off: While perfect for simple cases, this approach can become difficult to manage in a domain with a high degree of complexity and many intertwined business rules. It can lead to tangled code if overused.

Alternative 4: The Conceptual Shift - What Are You Really Modeling?

The final, and perhaps most elegant, solution is to step back and ask: what is the true responsibility here? Is the GroupChat really concerned with membership limits, or is it primarily about the messages being exchanged?

You might realize that “group membership” is a concept in itself. You could create a separate GroupMembership aggregate. This aggregate’s sole purpose would be to handle the invariant of whether a user can join a group. The GroupChat aggregate would then be responsible for managing messages, and the two would be distinct models. You don’t need one model to rule them all.

The Golden Rule: Model Rules, Not Relationships

Each of these four approaches has its own set of trade-offs. The key is to choose the one that fits your specific needs appropriately. Don’t feel forced to use an aggregate for everything just for the sake of dogma.

When you encounter a new business rule, start by asking: “Where can I reliably and efficiently enforce this?” Don’t start by asking, “How can I convert my database schema into an object model?”

The simple takeaway is this: Model your rules, not your relationships.

If you start with the rule you need to enforce, you’ll be better equipped to choose the right pattern for the job, whether it’s a lean aggregate, a transaction script, or a different model entirely.

Page last modified: 2025-08-26 18:26:15