May
31
2017

How do I enhance events with data that is not part of the Aggregate?

How do I enhance events with data that is not part of the Aggregate to answer the question? I can provide several possibilities. I will list them here in this blog post:

Include non-stateful attributes in the Aggregate

public class City extends AbstractAnnotatedAggregateRoot {
   // Name would normally not be included as it's not
   // necessary for the state of the object
   private String name;
   public City(AggregateIdentifier identifier) {
       super(identifier);
   }
   public City(AggregateIdentifier identifier, String name) {
       super(identifier);
       // Easy as the name is already an argument
       apply(new CityCreatedEvent(name));
   }
   public void remove() {
        // Here we can use the stored name to enhance the event
        apply(new CityRemovedEvent(name));
    }
   @EventHandler
   public void handle(CityCreatedEvent event) {
        // Store the name
        this.name = event.getName();
    }
}

 

Include immutable (!) data from other aggregates as an attribute

public class City extends AbstractAnnotatedAggregateRoot {
    // Reference to a country aggregate
    private UUID countryUUID;
     // Immutable (!) name of the country
    private String countryName;
     // Name would normally not be included as it's not
    // necessary for the state of the object
    private String cityName;
     public City(AggregateIdentifier identifier) {
        super(identifier);
    }
     public City(AggregateIdentifier identifier, UUID countryUUID, String countryName, String cityName) {
        super(identifier);
        // This event is easy as the names are already an arguments
        apply(new CityCreatedEvent(countryUUID, countryName, cityName));
    }
     public void remove() {
        // Here we can use the country and city name
        apply(new CityRemovedEvent(countryName, cityName));
    }
     @EventHandler
    public void handle(CityCreatedEvent event) {
        this.countryUUID = event.getCountryUUID();
        this.countryName = event.getCountryName();
        this.cityName = event.getCountryName();
    }
}

 

Query data in the command handler and pass it as an argument

@Named
public class CityCommandHandler {
     @Inject
    @Named("cityRepository")
    private Repository repository;
     @Inject
    private QueryService queryService;

    @CommandHandler
    public void handle(CreateCityCommand command) {

        // Checks the country reference exists and returns the name
        Country country = queryService.loadCountry(command.getCountryUUID());

        // Create the aggregate using the loaded country name
        City city = new City(new UUIDAggregateIdentifier(command.getCityUUID()), country.getUUID(), country.getName(), command.getCityName());
        repository.add(city);

    }

    @CommandHandler
    public final void handle(RemoveCityCommand command) {

        // Checks the country reference exists and returns the name
        Country country = queryService.loadCountry(command.getCountryUUID());

        // Load the city
        City city = repository.load(new UUIDAggregateIdentifier(command.getCityUUID()));

        // Use the name from the previous query
        city.remove(country.getName());

    }
 }

 

public class City extends AbstractAnnotatedAggregateRoot {

    // Reference to a country aggregate.
    private UUID countryUUID;

    // Note, that the country name is NOT stored
    // as it is considered mutable

    // Name of the city for the event
    private String cityName;

    public City(AggregateIdentifier identifier) {
        super(identifier);
    }

    public City(AggregateIdentifier identifier, UUID countryUUID, String countryName, String cityName) {
        super(identifier);
        // Easy as the name is already an argument
        apply(new CityCreatedEvent(countryUUID, countryName, cityName));
    }

    // NOTE: Method signature looks strange! Doesn't it?
    public void remove(String countryName) {
        // Here we can use the name from the argument
        // and the stored city name
        apply(new CityRemovedEvent(countryName, cityName));
    }

    @EventHandler
    public void handle(CityCreatedEvent event) {
        this.countryUUID = event.getCountryUUID();
        this.cityName = event.getCityName();
    }
 }

 

Include data in the command and pass it as an argument

@Named
public class CityCommandHandler {

    @Inject
    @Named("cityRepository")
    private Repository repository;

    @Inject
    private QueryService queryService;

    @CommandHandler
    public void handle(CreateCityCommand command) {

        // Checks the country reference exists and returns the name
        Country country = queryService.loadCountry(command.getCountryUUID());

        // Create the aggregate using the loaded country name
        City city = new City(new UUIDAggregateIdentifier(command.getCityUUID()), country.getUUID(), country.getName(), command.getCityName());
        repository.add(city);

    }

    @CommandHandler
    public final void handle(RemoveCityCommand command) {

        // Load the city
        City city = repository.load(new UUIDAggregateIdentifier(command.getCityUUID()));

        // Command includes the country name for the argument
        city.remove(command.getCountryName());

    }
 }

 

Query data in the aggregate's method using an injected service

@Named
public class CityCommandHandler {

    @Inject
    @Named("cityRepository")
    private Repository repository;

    @Inject
    private QueryService queryService;

    @CommandHandler
    public void handle(CreateCityCommand command) {

        // Checks implicitly the country reference and loads the name
        String countryName = queryService.loadCountryName(command.getCountryUUID());

        // Create the aggregate using the loaded country name
        City city = new City(new UUIDAggregateIdentifier(command.getCityUUID()), command.getCountryUUID(), countryName, command.getCityName());
        repository.add(city);

    }

    @CommandHandler
    public final void handle(RemoveCityCommand command) {

        // Load the city
        City city = repository.load(new UUIDAggregateIdentifier(command.getCityUUID()));

        // Inject the query service into the aggregate
        city.setQueryService(queryService);

        // Inside this method the name will be queried
        city.remove();
    }
}

 

public class City extends AbstractAnnotatedAggregateRoot {

    // Reference to a country aggregate.
    private UUID countryUUID;

    // Note, that the country name is NOT stored
    // as it is considered mutable

    // Name of the city for the event
    private String cityName;

    // Query service used to load missing data
    private transient QueryService queryService;

    public City(AggregateIdentifier identifier) {
        super(identifier);
    }

    public City(AggregateIdentifier identifier, UUID countryUUID, String countryName, String cityName) {
        super(identifier);
        // Easy as the name is already an argument
        apply(new CityCreatedEvent(countryUUID, countryName, cityName));
    }

    public void remove() {
        // Load the name and include it in the event
        String countryName = queryService.loadCountryName(countryUUID);
        apply(new CityRemovedEvent(countryName, cityName));
    }

    public void setQueryService(QueryService queryService) {
        this.queryService = queryService;
    }

    @EventHandler
    public void handle(CityCreatedEvent event) {
        this.countryUUID = event.getCountryUUID();
        this.cityName = event.getCityName();
    }

}

 

Query data in the aggregate's method using a method specific query service

This was suggested by Greg Young (Course in Hamburg, September 2011) to make more explicit that an aggregate method uses a query.

@Named
public class CityCommandHandler {

    @Inject
    @Named("cityRepository")
    private Repository repository;

    @Inject
    private QueryService queryService;

    @CommandHandler
    public void handle(CreateCityCommand command) {

        // Checks implicitly the country reference and loads the name
        String countryName = queryService.loadCountryName(command.getCountryUUID());

        // Create the aggregate using the loaded country name
        City city = new City(new UUIDAggregateIdentifier(command.getCityUUID()), command.getCountryUUID(), countryName, command.getCityName());
        repository.add(city);
    }

    @CommandHandler
    public final void handle(RemoveCityCommand command) {

        // Load the city
        City city = repository.load(new UUIDAggregateIdentifier(command.getCityUUID()));

        // Provide a method specific query service
        city.remove(new CityRemoveQueryService() {
            public String loadCountryName(UUID countryUUID) {
                // In this case we simply map the call to the common query service
            return queryService.loadCountryName(countryUUID);
            }
        });

    }

}

 

Caution

Never do any queries in an Event Handler method in an Aggregate! Replaying the events at a later time may else lead to different event content. 


Written by:
Allard's profile photo

Allard Buijze

Allard Buijze is the founder and chief technology officer at AxonIQ, a microservices communication platform for building event-driven, distributed applications, where he helps customers reach appropriate future-proof technical decisions.

A former software architect within the fields of scalability and performance, he has worked on several projects where performance is often a recurring theme. Allard is convinced that a good domain model is the beginning of contributing to the overall performance of an application and developed the Axon Framework out of this conviction.