In one of my previous blogs, I wrote about using Axon to cope with Set Based Consistency validation. It deals with the question: How can I validate a uniqueness constraint throughout the whole application using CQRS, having to deal with eventual consistency?
Discuss a better solution
Because set-based consistency validation is a hot topic, we discuss it a lot within AxonIQ. The solution I came up with by adding a command-side projection will not work in all cases because we are dealing with a distributed transaction (we need to update the event store and the projections). And by doing that, we are pulling the responsibility into the data layer, which we would like to prevent by using event sourcing.
So our next thought was, why not use an Aggregate that contains all unique values as the constraint to safeguard all incoming commands against the constraint? In the beginning, this may seem like an elegant solution. But the Aggregate will grow every time a specific value is accepted by applying an event. In the end, we will have a giant Aggregate which certainly is not the way to go.
The actor model and set-based consistency validation
In one of these discussions, my colleague Frédéric Gendebien proposed applying the actor model to solve this problem. This means that an aggregate is created for each unique value instead of having one big Aggregate.
Let’s assume we got this constraint: A User can only be registered when the email address is unique.
We need to create two aggregate types in this case. The first one is the EmailUniquenessCheck Aggregate, and the second one is the User aggregate. The EmailUniquenessCheck Aggregate has the emailAddress as the aggregate identifier.
A command is sent to the EmailUniquenessCheck Aggregate every time a user registers. The command handler should be implemented as a function (with a create_if_missing creation policy). A boolean property on the Aggregate states whether the email was claimed or not. If the email address was not claimed before, an EmailAddresApprovedEvent should be applied.
The moment the email address is approved, we should create the user. We can register the invariant (the User aggregate) using the Aggregatelifecycle.createNew() function in that (command handling) transaction.
If the user registration fails, the email address is not added to the claimed email addresses because the EmailAddressApproved event will not be stored. And when the email address is not unique, we will not register the user. This way, consistency is guaranteed.
How to release a claim for an email address
Now that we have the creation part covered, we can also try to solve the issue of updating the email address. If someone changes the email address, the former email address should be removed from the existing claimed email addresses.
The good news is that this can be done in two separate transactions. First, create the user with the new email address. If that command succeeds, the old email address can be released. This is why the command handling function is annotated with the create_if_missing creation policy. This command handler can be invoked not only if the email address is added to the set but also if the email address is not used anymore.
In this example, I used a String as a unique key, but a custom object can also be used to check a more sophisticated constraint. An elegant way is to use the object's hash code or concatenate the different values as the aggregate identifier.
We see that the actor model helps validate the uniqueness of a specific value against a set.
If you would like to see this in action and fiddle a bit with the concept, you can find example code in our code sample repo
A backend developer with more than 2 decades of experience and passionate about Java and the Spring Framework. Since 2017 my main focus is helping customers of AxonIQ to use and implement the Axon Framework as a consultant. I like to solve complex puzzles as easy as possible.