Jay
Jay

Reputation: 3

In domain driven design, should an aggregate keeps redundant or aggregated data from another aggregate to maintain invariants?

In domain driven design, should an aggregate keeps redundant data from another aggregate to maintain invariants?

For example, if we are designing a system with School and Student aggregate roots, there is a business rule: the total number of students in a school must not exceed 1000

There are two ways to enforce the rule when enrolling a new student to a school:

  1. do a select count from Student and check the number against 1000, might need to lock by school id

  2. maintain "student_count" property in School aggregate, validate the constraint inside the School domain, increment student_count first, then create a new Student. Need to update "student_count" property when students are deleted(left school or graduated).

Actually I prefer approach 2 as it enforces invariants of School domain, but my colleagues argue that approach 1 is simple to write and maintain.

I am looking for more discussions on this subject. Thanks

Upvotes: 0

Views: 413

Answers (2)

VoiceOfUnreason
VoiceOfUnreason

Reputation: 57277

For example, if we are designing a system with School and Student aggregate roots, there is a business rule: the total number of students in a school must not exceed 1000

The general term for this problem is set-validation.

If "must not exceed 1000" is a hard constraint (like: if this ever happens lives will be lost / we can get sued out of existence), then the answer is that the set itself must be completely contained under a single lock, which usually translates to "the set must be completely contained within one aggregate".

So that might mean that the "school" aggregate includes a set of student ids, and that when that set includes 1000 ids then enrollment is closed.

It's much more common that set constraints are soft ("when this happens we make less money"), in which case we can use discrepancy reports and escalation to trigger the domain processes that bring the set back into compliance.


There's been discussion recently on slack:ddd-cqrs-es about the fact that the constraints we use for "scheduling and planning" are often stricter than the constraints we use for "operations".

One key difference to recognize is this: when operating in the real world, the real world is the "book of record", our model is just working from a locally cached copy of information that may be wrong or out of date. We have to be cautious about designs that prevent the real world from correcting our data.

Upvotes: 0

Neil W
Neil W

Reputation: 9172

If you are guaranteed that there would be only one client interacting with the service at a time then option 1 would be the simplest check. The problem with option 1 in a multi-user environment is that pessimistic concurrency lock on the School table whilst the transaction is in process.

Option 2 would allow you to retrieve the current count from the database in the School aggregate and then ask the school aggregate to add the student and increment the count before saving back to the database. At this time, the school aggregate can check the count before adding the student and throw if count is already 1,000.

With Option 2 I would implement a concurrency token on the School so that optimistic concurrency can be implemented. If the school student count has been changed by another process you'll get a DbConcurrencyUpdate exception, which you can handle appropriately.

I'd catch that in the application layer and retry on behalf of the client. If the second time around (or third ...) you are successful, then all good. If the subsequent time you get a "Too Many Students" error then you can then throw back to the client.

Option 2 is definitely more DDD compliant - Keep the invariant checking in the school aggregate rather than application layer.

Upvotes: 1

Related Questions