Validating a Contact has a Unique Email in Axon

Viewed 518

I am curious to understand what the best practice approach is when using the Axon Framework to validate that an email field is unique to a Set of emails for a Contact Aggregate.

Example setup

ContactCreateCommand {
   identifier = '123'
   name = 'ABC'
   email = 'info@abc.com'
}

ContactAggregate {
   ContactAggregate(ContactCreateCommand cmd) {
      //1. cannot validate email
      AggregateLifecycle.apply(
                new ContactCreatedEvent(//fields ... );
        );
   }
}

From my understanding of how this might be implemented, I have identified a number of possible ways to handle this, but perhaps there are more.

1. Do nothing in the Aggregate

This approach imposes that the invoker (of the command) does a query to find Contacts by email prior to sending the command, allowing for some milliseconds where eventual consistency allows for duplication.

Drawbacks:

  • Any "invoker" of the command would then be required to perform this validation check as its not possible to do this check inside the Aggregate using an Axon Query Handler.
  • Duplication can occur, so all projections based from these events need to handle this duplication somehow

2. Validate in a separate persistence layer

This approach introduces a new persistence layer that would validate uniqueness inside the aggregate.

Inside the ContactAggregate command handler for ContactCreateCommand we can then issue a query against this persistence layer (eg. a table in postgres with a unique index on it) and we can validate the email against this database which contains all the sets

Drawbacks:

  • Introduces an external persistence layer (external to the microservice) to guarantee uniqueness across Contacts
  • Scaling should be considered in the persistence layer, hitting this with a highly scaled aggregate could prove a bottleneck

3. Use a Saga and Singleton Aggregate

This approach enhances the previous setup by introducing an Aggregate that can only have at most 1 instance (e.g. Target Identifier is always the same). This way we create a 'Singleton Aggregate' that is responsible only to encapsulate the Set of all Contact Email Addresses.

ContactEmailValidateCommand {
   identifier = 'SINGLETON_ID_1'
   email='info@abc.com'
   customerIdentifier = '123'
}
UniqueContactEmailAggregate {
   @AggregateIdentifier
   private String identifier;

   Set<String> email = new HashSet<>();
   
   on(ContactEmailValidateCommand cmd) {
     if (email.contains(cmd.email) == false) {
       AggregateLifecycle.apply(
                new ContactEmailInvalidatedEvent(//fields ... );
     } else {
       AggregateLifecycle.apply(
                new ContactEmailValidatedEvent(//fields ... );
        );
     } 
   } 
   
}

After we do this check, we could then re-act appropriately to the ContactEmailInvalidatedEvent or ContactEmailValidatedEvent which might invalidate the contact afterwards.

The benefit of this approach is that it keeps the persistence local to the Aggregate, which could give better scaling (as more nodes are added, more aggregates with locally managed Sets exist).

Drawbacks

  • Quite a lot of boiler plate to replace "create unique index"
  • This approach allows an 'invalid' Contact to pollute the Event Store for ever
  • The 'Singleton Aggregate' is complex to ensure it is a true (perhaps there is a simpler or better way)
  • The 'invoker' of the CreateContactCommand must check to see the outcome of the Saga

What do others do to solve this? I feel option 2 is perhaps the simplest approach, but are there other options?

1 Answers

What you are essentially looking for is Set Based Validation (I think here blog does a nice job explaining the concept, and how to deal with it in Axon). In short, validating some field is (or is not) contained in a set of data. When doing CQRS, this becomes a somewhat interesting concept to reason about, with several solutions out there (as you've already portrayed).

I think the best solution to this is summarized under your second option to use a dedicated persistence layer for the email addresses. You'd simply create a very concise model containing just the email addresses, which you would validate prior to issuing the ContactCreateCommand. Note that this persistence layer belongs to the Command Model, as it is used to perform business validation. You'd thus introduce an example where you not only have Aggregates in your Command Model, but also Views. And as you've rightfully noted, this View needs to be optimized for it's use case of course. Maybe introducing a cache which is created on application start up wouldn't be to bad.

To ensure this email addresses view is as up to date as possible, it's smartest to ensure it is updated in the same transaction as when the ContactCreatedEvent (which contains a new email address, I assume) is published. You can do this by having a dedicated Event Handling Component for your "Email Addresses View" which is updated through a SubscribingEventProcessor (a SEP). This would work as the SEP is invoked by the same thread publishing the event (your aggregate).

You have a couple of options when it comes to querying this model prior to sending the command. You could use a MessageDispatchInterceptor which only reacts on the ContactCreateCommand for example. Or, you introduce a Handler Enhancer which is dedicated to react ContactCreateCommand to perform this validation. Or, you introduce another command like RequestContactCreationCommand which is targeted towards a regular component. This component would handle the command, validate the model and if approved dispatches a ContactCreateCommand.

That's my two cents to the situation, hope this helps @vcetinick!

Related