vcetinick
vcetinick

Reputation: 2017

Validating a Contact has a Unique Email in Axon

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 = '[email protected]'
}

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:

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:

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='[email protected]'
   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

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

Upvotes: 2

Views: 809

Answers (1)

Steven
Steven

Reputation: 7275

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!

Upvotes: 3

Related Questions