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.