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.