Designing an Aggregate and choosing an Aggregate Root getting tricky for me all the time especially when it comes to ensuring right transactional contexts and consistency constraints so I'm wondering whether there are any practices that can make it easier.
By example
There is a student attendance tracking system that keeps records of student attendances of groups.
- Course is a top-level component, describes a generic info about the learning course (name, description)
- Every Course has Groups that describe groups a User must sign up to if he/she wants to attend Group Meeting.
- Every Group contains a list of Meetings that describe occurrings of the Group a User can attend. Is defined by (datetimeBegins, datetimeEnds and a set of Attendance objects).
- Attendance is a record holds info about a student attending. Contains a reference to the User and some statistical data.
Iteration #1
It seemed a good idea to design Aggregate boundaries to match the taxonomy above exactly, where the main pillar was the Course Aggregate Root:
It is nice because:
- Matches the domain taxonomy exactly
- Ensures domain constraints like "Groups cannot be created outside the Course" or "Meetings must be defined inside a Group".
But at the same time, it's very fat, hard to maintain and designed with false invariants in mind that caused transactional failures: nothing about creating a new Meeting item should logically interfere with editing/creating a course for example. It just doesn't scale. Moreover, usually, you may want to update Group by adding some Sessions having GroupId and it should not require fetching the whole Course.
Iteration #2
Then I came up with an idea to separate all main concepts into different ARs:
After all, I have not problems with transactions, but, unfortunately, it added more questions than solved problems:
- Since Group and Meeting are separate AR now, how to ensure nobody will create "deattached" from Course Group or Meeting outside the Group via their own repositories?
- Having
Meeting#attend(UserId userId)
method, how to ensure the user is eligible (signed up for the Group) for attending this meeting?
Ideas for the Iteration #3
I'm thinking about moving Meeting inside the Group AR, where I can put Group#attend(UserId userId)
method. But there is still a problem of ensuring the Group is created inside the Course only.
I was thinking about hiding (package private'ing/protected'ing) Group constructor and adding Course#createGroup(GroupParams p): Group
but I'm not sure if it's valid to mess two different concepts (Group and Course aggregate roots) in each other.
Moreover, it doesn't solve the problem users still can remove Group via its repository (a Spring's Repository#delete(Group)
in my case). I possibly can solve this by:
- having domain event sourcing i.e. Group repository will emit
GroupRemovedEvent
so the Course can subscribe to it and modify itsList<GroupId> groupIds
. - having a reverse reference from Group to Course, but it may become inconvenient because use case
get all groups of the course by CourseId given
is a day-to-day operation.
I would appreciate any ideas on this matter. Thank you.