2

I'm looking for some advice on a system I'm designing, at its core it will allow users to create/join/manage groups of users.

I thought it would be a good idea to try and apply some DDD principles, which has led me to create following domain models, I'm currently treating a Group as the aggregate root.

public class Group
{
    public Guid ID { get; set; }
    public string Name { get; set; }
    public bool IsClosed { get; set; }
    public IEnumerable<Membership> Memberships { get; set; }
}

public class Membership
{
    public Guid ID { get; set; }
    public Guid GroupID { get; set; }
    public string UserID { get; set; }
}

You'll notice I don't have the concept of a 'User' in this system, and that is because users are managed by a different system altogether (it won't be possible to 'merge' the two systems either). I've also simplified the models here for the sake of brevity.

There are a few concerns I have with this approach and how I'm going to tackle some of the domain invariants:

  1. Hydrating a Group from the repository will require loading in all of the Membership entities, of which there could be many (the maximum will be 1000), this seems a bit ugly however a Membership can't exist outside the concept of a Group so I don't think it's a candidate to an AR.
  2. There is an invariant that limits the number of Groups a user can belong to (currently 50). I've seen other examples where the suggestion was to query a count from the repository before persisting the change, but this would introduce a race condition since the system will be used concurrently.
  3. I could indeed model Membership as its own AR, and then have a MembershipCount property on each Group (to satisfy the maximum Membership invariant), but this would mean updating two ARs in a single transaction (one to add a Membership, and one to increment the MembershipCount on the Group). This approach also seems like to could introduce a fair amount of contention if using something like optimistic concurrency control if multiple users are attempting to join the same Group.
  4. Users cannot join 'Closed' Groups. This invariant would be straightforward to satisfy if Memberships were a collection inside Group, because we could rely on OCC if a Group has been closed whilst adding a new Membership.

I understand there's likely to be some trade-offs that need to be made here, I don't think it's possible satisfy all of these invariants whilst maintaining complete consistency, but It'd be great to hear any suggestions :)

1 Answer 1

0

The Group is an aggregate root. Membership make only sense in relation to the group, and should therefore belong to that aggregate, that will have to enforce the invariants related to the group memberships, such as denying adding memberships to closed groups.

Technically, the hydration of the Group does not require all its Memberships to be in memory. You could implement a lazy load for the Memberships.

For practical reasons, you'll probably need a User in your model as well, that acts as proxy for the User managed in the other system. The User would be root of its own aggregate, because User exists independently of the the Group and should be accessible directly without passing through the aggregate root.

Your narrative suggest that you need bidirectional navigation: a User would maintain a collection UserMemberships to Groups to which it is member. This aggregate would be responsible to enforce the User invariants in your model, such as the limits to the number of groups a user can be member of.

Since adding a user to a group involves two different aggregates, each being responsible of its own invariants, this operation would be a service in the DDD meaning.

2
  • 1
    Thanks for the great answer. I came to a similar solution after thinking about the problem for a while. The only aspect that leaves a bad taste is the proxy User, I wouldn't want consumers of this system to have to explicitly create this 'fake' User, so I thought about having a FindOrCreate method on the repository that would be invoked when the existence of the proxy User is necessary. Perhaps there's a better name for this entity that would communicate it's purpose a bit better. Aug 27, 2022 at 1:18
  • 1
    I also toyed with the idea of not having the proxy User at all, and instead relying on locking at the database level to enforce the invariant, but that feels like I'm allowing business logic to leak into the persistence layer. E.g. In SQL Server teminology, performing a SELECT with an UPDLOCK over the UserMembership entities. The obvious down-side of this approach is that if I were to switch to a different DB vendor I might not be able to achieve the same consistency guarantees. Aug 27, 2022 at 1:20

Your Answer

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

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