How to handle concurrent constraints across aggregate roots
Asked Answered
S

3

6

I'm afraid I already know the answer, but I'm hoping that somebody can provide an alternative solution that haven't found before. As always doing DDD according to Effective Aggregate Design is more difficult than I thought, but here's my scenario.

  • We have two ARs, User and RoleGroup
  • A User can be granted a particular RoleGroup and thereby obtains the permissions provided by the Roles (a collection value object) in that role group. The identity of the role group is kept in the User AR as another VA.
  • When a RoleGroup is removed from the system, we raise a domain event that a handler uses to find all users referring to that RoleGroup and to remove the reference. The corresponding projection denormalizer will use that same event to update the effective roles of the User. This is a combination of the individual roles granted to that User and the roles of all granted RoleGroups.
  • This doesn't have to be transactional (iow it can be eventually consistent).
  • We use Event Sourcing using Jonathan Oliver's EventStore 3.0 and elements from Lokad.CQRS and NCQRS.

So, in theory, when one request (it's an ASP.NET MVC app) is executing the scenario mentioned above, it is possible that another request is granting that same RoleGroup to a User. If that happens just after the above mentioned domain event handler scans for users related to that RoleGroup, that request will complete. At that point you have a RoleGroup that is deleted (albeit not physically) and a User that is still holding the identity of that RoleGroup.

How do you prevent this? We're currently looking at making the identity of the Users granted a particular RoleGroup part of that RoleGroup AR, so that deleting a RoleGroup and granting it to a user will cause a optimistic concurrency conflict. But somehow, this doesn't feel like the correct solution.

Sialkot answered 29/11, 2012 at 12:44 Comment(3)
I would think that as soon as a user group is removed, a grant command for that group should fail, even if the handler for the removed event hasn't yet completed, or even started. This failure would occur in the handler for the grant command, which would first determine whether the group exists before associating a user with it. However, given that a domain event is used I'm assuming the removed event is handled on the same thread and the same tx as the removal of the user group AR?Ettie
That's the problem. The removal of the RoleGroup and the grant of it will happen on different threads. So what you are suggesting isn't easy to do.Sialkot
Why do you think an optimistic concurrency conflict isn't a correct solution? Suppose that both remove and grant are sent, they both load the same user group AR and run some logic. If the remove handler commits before the grant handler, the grant handler would encounter an concurrency violation exception. The same holds for the reverse scenario where the grant commits before the remove. This seems like a suitable use case for optimistic concurrency.Ettie
U
1

This is similar to how uniqueness constraints CAN be solved.

Suppose there's a projection with both rolegroups and users that has SERIAL behavior. When rolegroups get archived (i.e. they can no longer be used), the reactive bits sitting on top of the projection can notify all the users that have been granted said rolegroup that they are no longer part of it. When concurrently this archived rolegroup is granted to a user (or a set of), the serial nature of the projection can be leveraged to tell this user too that they are no longer part of the group.

All that said, this is just housekeeping. It's only when the rolegroups and users get used that a correct view is important. Since I presume rolegroups will carry an IsArchived bit, I can safely filter them out at that time, without worrying about some dangling edge-case for which we still have to prove that it has to be resolved in an automated way.

As an aside, scanning the event log would also reveal this situation, i.e. are there any users granted a rolegroup that was archived before that point in time (or around that point in time)? An admin could resolve this by issueing a compensating command to the user aggregate.

"It depends" TM

Edit: I've given a technical solution to this problem. I would encourage other readers to explore different ways of modeling & solving these kinds of problems. Sometimes, perhaps even most of the times, the answer isn't technical at all. YMMV.

Unclassified answered 29/11, 2012 at 14:1 Comment(8)
What do you mean "the reactive bits sitting on top of the projection"? BTW, the user projection will contain the effective roles. These are updated as soon as a role (group) is granted/revoked.Sialkot
Event handlers part of the workflow that send out commands based on the state of the projection AND the event being handled.Unclassified
So you're saying a denormalizer (the one handling the events), could actually submit a new command?Sialkot
It's not really a denormalizer in the traditional sense (i.e. for UI purposes). I'm talking about a dedicated projection with some reactive (i.e. they send commands indeed) event handlers (very akin to saga/workflow kinda thing) on top to solve this particular problem. It's relatively costly to do so IMO and should be weighed against the other options you have.Unclassified
Makes sense. And that dedicated projection would only be used to enforce this constraint? And I assume with 'SERIAL behavior' you mean a table with Serializable transaction behavior?Sialkot
More like a queue in front of the projection (not a database isolation level).Unclassified
Ah that explains why Udi is always referring to Sagas. They work nicely together with NServiceBusSialkot
Sure, an NSB saga would do the job as well.Unclassified
S
1

Why are you adding a RoleGroup reference to the User aggregate? Are there any invariants on the User that use this information?

I imagine this can be modelled much simpler by granting the RoleGroup to the User via the RoleGroup aggregate, emitting something like a RoleGroupGrantedToUser event. When a RoleGroup is removed it emits a RoleGroupRemoved event. After this event the RoleGroup no longer accepts new Users.

Suggestibility answered 29/11, 2012 at 13:15 Comment(2)
Yeah, that's the approach we're considering right now. But it does mean that without any special event merging features, assigning two users the role group concurrently will cause an optimistic concurrency issue.Sialkot
Assuming that there are a lot of users and the fact that rolegroups might be long-lived, that could start piling up quickly wrt the number of events (not a problem per sé). Granted, since the rolegroup is the serialization boundary for the given invariant, it seems like a natural candidate.Unclassified
U
1

This is similar to how uniqueness constraints CAN be solved.

Suppose there's a projection with both rolegroups and users that has SERIAL behavior. When rolegroups get archived (i.e. they can no longer be used), the reactive bits sitting on top of the projection can notify all the users that have been granted said rolegroup that they are no longer part of it. When concurrently this archived rolegroup is granted to a user (or a set of), the serial nature of the projection can be leveraged to tell this user too that they are no longer part of the group.

All that said, this is just housekeeping. It's only when the rolegroups and users get used that a correct view is important. Since I presume rolegroups will carry an IsArchived bit, I can safely filter them out at that time, without worrying about some dangling edge-case for which we still have to prove that it has to be resolved in an automated way.

As an aside, scanning the event log would also reveal this situation, i.e. are there any users granted a rolegroup that was archived before that point in time (or around that point in time)? An admin could resolve this by issueing a compensating command to the user aggregate.

"It depends" TM

Edit: I've given a technical solution to this problem. I would encourage other readers to explore different ways of modeling & solving these kinds of problems. Sometimes, perhaps even most of the times, the answer isn't technical at all. YMMV.

Unclassified answered 29/11, 2012 at 14:1 Comment(8)
What do you mean "the reactive bits sitting on top of the projection"? BTW, the user projection will contain the effective roles. These are updated as soon as a role (group) is granted/revoked.Sialkot
Event handlers part of the workflow that send out commands based on the state of the projection AND the event being handled.Unclassified
So you're saying a denormalizer (the one handling the events), could actually submit a new command?Sialkot
It's not really a denormalizer in the traditional sense (i.e. for UI purposes). I'm talking about a dedicated projection with some reactive (i.e. they send commands indeed) event handlers (very akin to saga/workflow kinda thing) on top to solve this particular problem. It's relatively costly to do so IMO and should be weighed against the other options you have.Unclassified
Makes sense. And that dedicated projection would only be used to enforce this constraint? And I assume with 'SERIAL behavior' you mean a table with Serializable transaction behavior?Sialkot
More like a queue in front of the projection (not a database isolation level).Unclassified
Ah that explains why Udi is always referring to Sagas. They work nicely together with NServiceBusSialkot
Sure, an NSB saga would do the job as well.Unclassified
M
0

To prevent this you can make it transactional i.e. make it impossible to grant a deleted RoleGroup to a User by some locking mechanism. But that would only complicate things and, as you noted, is not required.

When you assign a RoleGroup to a User, I suppose you have a similar projection denormalizer to update the user's effective roles as well. You could check there if a granted RoleGroup still exists at that point, and if it doesn't, remove the reference from the User. Then the effective roles on a user should eventually be consistent.

Molluscoid answered 29/11, 2012 at 13:7 Comment(1)
Correct, and in fact, that's how we handle projections. But in that case my domain model would not be consistent anymore.Sialkot

© 2022 - 2024 — McMap. All rights reserved.