I have the following Ubiquitous language defined by our domain expert: A User can find UserGroups and add himself to it. A user can query a list of UserGroups where he is member of and remove himself from it. UserGroups are created by Users, where the creator is the first member. When there is no more User in the UserGroup, the UserGroup should be deleted from the views.

As UserGroups can become pretty big, by following "Effective Aggregate Design", I came to the following {AR}'s:


All fine, except how to make the last rule (When there is no more User in the UserGroup, the UserGroup should be deleted from the views.) work with this model?

I've considered a saga that listens to UserGroupMemberLeft event, queuing the nr of members and if 0 delete the group. But as it queries the read-model, I have an ordering problem as the read-model is eventually updated as well. How to solve this? Should I consider a ReadModelUpdated event as well?

  • 1
    Why is UserGroupMember an AR of its own? It can exist as a table in the database, but only because that is how databases store the many-many relation between User and UserGroup. – Bart van Ingen Schenau Nov 3 '15 at 14:04
  • If this is not an AR, then there must be a collection inside UserGroup of Users. I always see this in those oversimplified examples of DDD, but in reality this would never perform or scale as you have a huge trx-boundary. – Pepster Nov 4 '15 at 9:36
  • If UserGroupMember really is an AR, then you aren't going to be able to support retirement of UserGroups in a transactionally consistent way. I push hard on the assumption that it is a real thing - what business invariant is it responsible for? – VoiceOfUnreason Dec 10 '15 at 20:19
  • For instance: does User.createGroup() create two aggregate roots in the same transaction? If so, is that a good idea? – VoiceOfUnreason Dec 10 '15 at 20:42

All fine, except how to make the last rule (When there is no more User in the UserGroup, the UserGroup should be deleted from the views.) work with this model?

It's a constraint on the query executed by the read model to create the view.

This may mean that you need to update the read model to support the constraint. For instance, in your original implementation, you might have have supported the view of user groups with a Set, where each group would be inserted into the set when it was created.

With the new requirement, the read model now needs to track total membership. So you replace the Set with a Map, with you used to track the count of users added and removed from the group. When you need a view of groups, you enumerate the entries in the hash to find the groups that satisfy the constraint (subscribedMembers > unsubscribedMembers).

Note that these details live purely in the read model -- the requirement as described didn't impose any restrictions on the write model. Based on your comment about UserGroupMemberLeft, it looks like you are trying to introduce end of life to the model.

As pointed out by @JDT, some aggregate needs to be responsible for the end of life. What state determines if the delete of a group is allowed? The aggregate that controls that state is the one that should be responsible for the end of life operation, since it's the only one that knows if the retirement of the group is allowed.

I don't know your domain, but I'd guess that the group itself contains the required state. For instance, if you aren't allowed to retire a group more than once, then it's probably the group that is tracking that. Similarly, if you are only allowed to retire empty groups, its probably the state of the group that is tracking that.

So I assume the group is responsible for its own retirement. At this point, you have two options available to you. One is a process manager, which observes the membership counts and sends a command to retire the group when it observes that there are no members. Because of eventual consistency, this command might be rejected -- for instance, if a new member joins the group after the last one has left.

If you need to retire the group in a transactionally consistent way, then the group locks itself against further enrollment as part of the transaction when the last member leaves. For example, if the write model is broadcasting events to the read model, then the departure of the last user would generate two events, one describing the user leaving, and a second describing the retirement of the group.

Since the group locks itself when the last member leaves, you don't need to worry about the race condition.

Done this way around, your read model can use the Set implementation from earlier, adding groups to the set when they are first created, and removing them when the group is retired.

  • Interesting! For the self aware retirement, how does the Group aggregate know that the last user left in case Member is an aggregate on its own? I guess a ProcessManager is inevitable here...? – Pepster Dec 11 '15 at 15:25
  • I think so too. Which might be another hint that Member shouldn't be an AR of its own, given the rules of your domain. – VoiceOfUnreason Dec 11 '15 at 16:27

The thing lacking in your domain language is the concept of WHO should delete the group. Users can create groups and add themselves to it, which puts the responsibility for those actions clearly on the users. But you go on to say that 'groups should be deleted'. From a business perspective, this makes no sense.

If your requirement is rephrased to "When there is no more User in the UserGroup, the User should delete the group " it becomes obvious who's responsibility it is to remove the group.

  • Perhaps it makes no sense from business side of view, but there is a requirement that the system should not show zero-membered groups. Which could be translated into empty groups should be deleted. Call it otherwise; made invisible. – Pepster Nov 3 '15 at 13:38
  • It's a valid requirement, but what I'm trying to say is that groups do not simply disappear. Something should be responsible for making these disappear, and that thing needs a name. Compare it to a coffee machine at work: employees can get a cup of coffee and when the coffee runs out, a new pot should be brewed. It won't brew itself! So that requirement would be better phrased as 'the employee who gets the last cup should start a new one'. Can you express this requirement so that it is clear WHO the actor is that performs the action? – JDT Nov 3 '15 at 14:28
  • 1
    @JDT very insightful question. Sometimes it turns out this actor is simply "the system". In which case there are good chances that eventual consistency is appropriate. See dddcommunity.org/wp-content/uploads/files/pdf_articles/… page 9 – guillaume31 Jul 19 '16 at 13:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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