Domain-Driven Design – How to Model Validation and Enforce Invariants Across Aggregates

aggregatedomain-driven-designinvariantsmodeling

I have two models in my current design, student and group. Student and group are both aggregate roots.

A student can be added to a group (method on group aggregate root), and it can be active for either the whole duration of the group or part of it. The group has an invariant which says that "all students added to a given group has to either be active for the full duration of the group, or have start- and end-dates that are inside the groups start and end dates. These connections (group.student) are saved as a child entity (0..*) that belongs to the groups aggregate. So far so good.

However, Group also has an property called "course-code", which is a value object conatining a specific code along with additional (for this example irrelevant) data.

The part I'm struggling with.
One business rule of the domain and system is that a student can only have one active group-connection per course-code at any given date. By that I mean that if a student is added to a group the Group.AddStudent(student, daterange)-method has to check if the student has an active connection to another group with the same course-code at the supplied daterange. This is data that doesn't belong to a single group aggregate and neither to the student as you currently can't query student for its active groups by (f.eg) student.groups. I've intentionally modeled it like this to avoid a bi-directional relationship.

What I've thought about

  1. Put the whole action (add to group) in a domain service that can ask repositories for the data and enforce /hold invariants.
  2. Pass all groups the student is currently connected to to the AddStudentToGroup-method (or possibly just groups on the same course-code).
  3. Add a property to a student which would hold all courses it's currently studying with their respective dateranges. This would allow method to ask the passed in student if the supplied date is overlapping any existing range or not. This also has the upside of enabling a student to stand more on its own instead of having a list of group-ids that needs to be loaded each time you require some data about them.

Someone else who's encountered a similar situation? How would/did you solve it?

Best Answer

I'm going to forego the discussion about artificial invariants and assume there is a compelling reason why such a convoluted invariant must exist. I will also assume there is absolutely no way to re-assess the rules to come up with a simpler system by way of limiting the possible combinations of active/inactive + courseId + date range. ...

Okay. Your choices are limited here (and are all really the same thing packaged in different ways). Option one is to wrap everything up in a single aggregate, Course, which has a collection of Group and Student and allow it to be the arbiter of how a Student gets into a Group. Of course this will mean loading a bunch of unnecessary and potentially expensive state each time you intend to change any one of these connections. Depending on the size of your system, the simplicity of a single aggregate may not be feasible.

Option two is to model the appropriate slice(s) of state in order to limit the amount of data loaded into your system to what is necessary for the desired use case. That is, explicitly model the required slices of state necessary to carryout your use case, populate them, and coordinate them. There is no reason to issue a query from a domain service because you will know up front that the AddStudentToGroup use case requires all of the groups in which a student is currently active for a given date range as well as the group to which you intend to add the student. These queries can be issued first so only the data (model) needs to be passed to the domain service for coordination.