There are several possibilities to do this:

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();
}
}

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();
}
}

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 the an Event Handler method in an Aggregate! Replaying the events at a later time may else lead to a different event content.

Sign up for our newsletter

monthly updates about new product releases and invitations to AxonIQ events