Enhancing 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.

Allard Buijze
Founder and Chief Technology Officer. Allard is a global thought-leader on event sourcing. He is a recognised expert with more than 20 years experience, including microservices, event sourcing and event-driven architecture. Allard advocates for better collaboration between developers and business.
Allard Buijze

Share: