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

January 11, 2012

There are several possibilities to do this:

  1. Include non-stateful attribute 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();
        }
    }
  2. Include immutable (!) data from other aggregate as 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();
        }
    }
  3. Query data in the command handler and pass it as an argument
    @Named
    public class CityCommandHandler {
    
        @Inject
        @Named("cityRepository")
        private Repository<City> 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();
        }
    
    }
  4. Include data in the command and pass it as an argument
    @Named
    public class CityCommandHandler {
    
        @Inject
        @Named("cityRepository")
        private Repository<City> 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());
    
        }
    
    }
  5. Query data in the aggregate’s method using an injected service
    @Named
    public class CityCommandHandler {
    
        @Inject
        @Named("cityRepository")
        private Repository<City> 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();
        }
    
    }
  6. 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<City> 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);
                }
            });
    
        }
    
    }
    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));
        }
    
        public void remove(CityRemoveQueryService queryService) {
            // Load the name and include it in the event
            String countryName = queryService.loadCountryName(countryUUID);
            apply(new CityRemovedEvent(countryName, cityName));
        }
    
        @EventHandler
        public void handle(CityCreatedEvent event) {
            this.countryUUID = event.getCountryUUID();
            this.cityName = event.getCityName();
        }
    
        // Explicit query service for the delete method
        public static interface CityRemoveQueryService {
            public String loadCountryName(UUID countryUUID);
        }        
    
    }

Caution

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

« To the Frequently Asked Questons