2

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?

6
  • One thing you should really be paying attention to in your design - what does it cost the business if there is a violation of this invariant. Do your domain experts already have mitigations available? Commented Jan 21, 2019 at 21:15
  • You're speaking of aggregate roots, but not of bounded contexts. Are these two aggregate roots in the same bc?
    – Erik Eidt
    Commented Jan 21, 2019 at 22:12
  • In DDD you don’t have to get it right the first time. Don’t overthink your solution. Go with what feels best for now. Reflect later. Keep the good parts, refactor the bad. I would probably start with your third approach.
    – Rik D
    Commented Jan 21, 2019 at 22:26
  • @VoiceOfUnreason the violation of invariant causes headaches later on in the life cycle when it comes to grading students. There are currently no mitigation available or planned as I'm refactoring a legacy application, hence why I'm trying to model it as an invariant. I've also talked to experts who says this is an invariant that makes perfect sense for the domain.
    – CitiZen
    Commented Jan 21, 2019 at 23:01
  • @ErikEidt at this point I'm not entirely sure, but my gut feeling is that they will indeed belong to the same BC. At least the two I'm speaking of here. I will most likely find student entities / ARs in other BCs later on when it comes to invoicing schools per active student f.eg
    – CitiZen
    Commented Jan 21, 2019 at 23:04

1 Answer 1

0

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.

1
  • You are right it is a convoluted and complex invariant. It has its roots in data integrity and consistency for reports to different governmental institutions and internal statistics. If this invariant is removed the depending systems will have a very hard time presenting consistent data that the user can follow (which is the current situation we're moving away from). I really like option two. I can have the application service retrieve the data and pass to the domain object / service. Something like my third suggestion. Thank you!
    – CitiZen
    Commented Jan 21, 2019 at 23:17

Not the answer you're looking for? Browse other questions tagged or ask your own question.