Defining Command Handlers
Within CQRS applications command handling is responsible for validating business rules, before applying changes to the application's state by means of new events. In order to execute command handling logic, it is necessary to reconstruct the write model, in order to be able to decide, whether the command is applicable or not. This is why command handlers may not be executed directly, but will be executed by the command router, instead, as shown in command handling.
Commands
Commands express the intent and any information required to execute the associated command handling logic. Commands must
implement Command, which:
- identifies the command subject used to source the events to reconstruct the write model
- an optional
SubjectConditionspecifying conditions with respect to the subject's existence.
An example of a pristine subject command used to purchase new (not yet existing) books within a library, may look as follows:
public record PurchaseBookCommand(
String isbn,
String author,
String title,
long numPages
) implements Command {
@Override
public String getSubject() {
return "/books/" + isbn();
}
@Override
public SubjectCondition getSubjectCondition() {
return SubjectCondition.PRISTINE;
}
}
Command Handler Definitions
OpenCQRS requires developers to provide CommandHandlerDefinitions,
encapsulating the command handling logic. These need to be registered with a CommandRouter,
in order to execute commands. CommandHandlerDefinition requires the following parameters
for its construction:
- A
java.lang.Classidentifying the type of the write model to be reconstructed prior to the actual command execution. This class is used to identify the state rebuilding handlers used to reconstruct the write model, i.e. those with the same instance type. - A command class implementing
Commandexpressing the intent and any information required to execute the command. - A
CommandHandlerencapsulating the command handling logic, which will be executed with the given command, once the write model has been reconstructed. - An optional
SourcingModespecifying which events need to be sourced in order to reconstruct the write model.
Command Handlers
There are three different types of CommandHandler that may be extended
to define the actual command handling logic; all of them are functional interfaces:
ForCommandif accessing theCommandis sufficient. This is typically used for creational commands in combination withSubjectConditionPRISTINE.ForInstanceAndCommandif additional access to the write model is required.ForInstanceAndCommandAndMetaDataif additional access to any command meta-data is required.
All three types accept an additional CommandEventPublisher for publishing new events
as part of the command handling logic. In addition, all types require the developer to specify the following generic types:
- the type of the write model to be sourced
- the command type to be handled
- the return type, or
java.lang.Voidfor command handlers returningnull
Choosing a CommandHandler Type
The choice of a suitable CommandHandler is for syntactical reasons only, especially
when using Java or Kotlin lambda expressions to implement them. The choice has no effect on the command handling workflow itself,
nor the presence of meta-data or the reconstructed write model.
Registration
CommandHandlerDefinitions need to be created and registered with the
CommandRouter, before commands can be executed.
Command Ambiguity and Inheritance
All CommandHandlerDefinitions registered with the same
CommandRouter instance must be non-ambiguous with respect
to their Command for the router to know, where commands
need to be routed to. Command inheritance is ignored for that matter, i.e. commands A and B extending A will
be treated as distinct commands.
Manual Registration
A list of CommandHandlerDefinition can be registered with a
manually configured
CommandRouter programmatically as in the following example:
public static CommandRouter commandRouter(EventRepository eventRepository) {
CommandHandlerDefinition<Book, PurchaseBookCommand, Void> purchaseCmdDef = new CommandHandlerDefinition<>(
Book.class,
PurchaseBookCommand.class,
(CommandHandler.ForCommand<Book, PurchaseBookCommand, Void>) (command, publisher) -> {
if (command.numPages <= 0) {
throw new IllegalArgumentException("Number of pages must be greater than 0.");
}
publisher.publish(
new BookPurchasedEvent(
command.isbn(),
command.author(),
command.title(),
command.numPages()
)
);
return null;
}
);
return new CommandRouter(
eventRepository,
eventRepository,
List.of(purchaseCmdDef),
List.of(/* (1)! */)
);
}
StateRebuildingHandlerDefinitions omitted, see
Spring Boot based Registration
When using the Spring Boot auto-configured command router
CommandHandlerDefinitions can be defined as Spring beans, in order to be
auto-wired with the CommandRouter instance created by
the CommandRouterAutoConfiguration.
Command Handling Test Support
It is recommended to define CommandHandlerDefinition Spring beans
within classes annotated with CommandHandlerConfiguration, which in turn
is meta-annotated with org.springframework.context.annotation.Configuration. This enables
command handling tests to automatically detect and load them without
starting a full-blown Spring context, instead using the CommandHandlingTest test
slice.
Command Handler Definition using @Bean Methods
CommandHandlerDefinitions can be defined using org.springframework.context.annotation.Bean
annotated methods, as shown in the following example.
@CommandHandlerConfiguration
public class BookHandlers {
@Bean
public CommandHandlerDefinition<Book, PurchaseBookCommand, Void> purchaseBookCommandVoidCommandHandlerDefinition(BookValidator bookValidator) {
return new CommandHandlerDefinition<>(
Book.class,
MyBookCommand.class,
(CommandHandler.ForCommand<Book, PurchaseBookCommand, Void>) (command, publisher) -> {
bookValidator.checkValidPurchaseRequest(command);
publisher.publish(
new BookPurchasedEvent(
command.isbn(),
command.author(),
command.title(),
command.numPages()
)
);
return null;
}
);
}
}
The CommandHandler hence may access additional auto-wired beans (e.g. BookValidator), which are defined
as auto-wired dependencies within the @Bean annotated method signature.
Command Handler Definition using Annotations
CommandHandlerDefinitions may be defined - even in combination with @Bean definitions -
using the CommandHandling annotation on any suitable command handling method. This greatly
simplifies the definition, since developers are relieved from explicitly instantiating CommandHandlerDefinition
and choosing a proper CommandHandler subtype, as shown in the following example:
@CommandHandlerConfiguration
public class BookHandlers {
@CommandHandling
public void purchase(
PurchaseBookCommand command,
CommandEventPublisher<Book> publisher,
@Autowired BookValidator bookValidator
) {
bookValidator.checkValidPurchaseRequest(command);
publisher.publish(
new BookPurchasedEvent(
command.isbn(),
command.author(),
command.title(),
command.numPages()
)
);
}
}
The following rules apply with respect to CommandHandling annotated definitions,
enforced by CommandHandlingAnnotationProcessingAutoConfiguration during Spring
context initialization:
- The method must be defined within a Spring bean with singleton scope.
- The method name can be chosen arbitrary.
- The method arguments can be ordered arbitrarily, as needed.
- The method arguments must be non-repeatable and un-ambiguous.
- One of the parameters must be of type
Command. - At least one of the following two parameters needs to be defined:
CommandEventPublisheridentifying the write model instance type via its generic type- a Java object representing the write model instance
- An optional parameter of type
java.util.Map<String, ?>may be defined to access the command meta-data. - Any number of
@Autowiredannotated parameters may be defined for injection of dependent Spring beans.
Lazy Resolution of Dependencies
Method parameters annotated with @Autowired are lazily injected upon command execution. Accordingly, failures to resolve
those dependencies will not occur during Spring context initialization but when actually executing commands for the first time, resulting
in a NonTransientException.