10

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.

enter image description here

  1. Course is a top-level component, describes a generic info about the learning course (name, description)
  2. Every Course has Groups that describe groups a User must sign up to if he/she wants to attend Group Meeting.
  3. 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).
  4. 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:

enter image description here

It is nice because:

  1. Matches the domain taxonomy exactly
  2. 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: enter image description here

After all, I have not problems with transactions, but, unfortunately, it added more questions than solved problems:

  1. 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?
  2. 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

enter image description here

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 its List<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.

5
  • Not all constraints can be expressed in (the static structure of) your domain model. For example, you can't capture a constraint like "attendance records can't be added before the start date of the meeting". These constraints have to be ensured in other ways. Dec 7, 2017 at 11:33
  • @BartvanIngenSchenau can you name these ways, please?
    – ovnia
    Dec 7, 2017 at 13:39
  • The primary way of ensuring such constraints is by checking for them in the code that attempts to make the modification (and limiting which code writes to the database). Dec 7, 2017 at 13:42
  • 1
    Must be a home work problem. Exactly the same question here. Dec 19, 2017 at 21:38
  • I am going to use this as homework on DDD just so I have more ammunition to argue against this way, way overly-complicated, convoluted method going forward...
    – VSO
    Nov 14, 2022 at 14:04

3 Answers 3

0

As far as I know there are two things your should consider by choosing aggregates.

  1. Invariants: For example you have 2 properties x and y and the sum of them must be always 6. Obviously you cannot change them independently, because you would end up in invalid states. After identifying your invariants you will know you consistency boundaries better.
  2. How much data you want to load into memory: For example if your aggregate contains a massive list of entities, you need to load all of them you want to modify it, because otherwise it is hard to ensure that all of the invariants are fulfilled. In theory you can lazy load your related entities, so if there is no invariant trying to use them, then they won't be loaded, but still it might be better to add another aggregate for those entities and refer to them only with ids in your first aggregate.
0

I think it will not hurt if you think in an Object Oriented way and what actions are performed:

  1. A user can have a meeting
  2. A group can attend a Course
  3. A group of users can attented a meeting.

Then I would set my aggregate routes as:

Meeting and Course and then rest would be entities / value objects

If there is an action associated wth User. F.ex. User logs in then you may as well have a User aggregate in the context of Login.

0

Your data model appears to be incomplete.

  • On the one hand you do use a bridging entity, Attendance Record, for Users attending each Meeting; On the other hand you don't resolve Group membership. Membership is usually a distinct thing with a membership Start and End date.
  • Your titles for your entities are too abstract. For example, "Group" should be "Study Group".

Then your Boundaries are:

  1. "Study Group" is an aggregate root, managing Study Group Membership, Study Group Meetings, and Study Group Attendance.
  2. Courses are a separate aggregate root, but treated as a simple (lookup) value in the Group context
  3. Users are a separate aggregate root, but treated as a simple (lookup) value in the Group context

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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