Enhancing events with data that is not part of the Aggregate?

This blog describes the way you can enhance events with data that is not part of the Aggregate.

Published May 31, 2017

Published May 31, 2017

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.