Defining State Rebuilding Handlers
Within CQRS applications state rebuilding logic is responsible for
reconstructing the write model (state)
in order to be able to handle commands, which need to decide if the command
is applicable or not with respect to that state. The command router
therefore always executes all relevant StateRebuildingHandlerDefinitions
based on the sourced events prior to executing the command.
Events and Write Models
Events represent facts or state changes within a CQRS/ES application. Accordingly, they can be used to
reconstruct the state needed to handle a command.
This is referred to as the write model within CQRS and any Java class (except java.lang.String) may be used to represent it,
as shown in the following Book class, which represents books within a library, their lending status, and any damaged pages:
public record Book(
String isbn,
long numPages,
Set<Long> damagedPages,
Lending lending
) {
// ...
}
Why is there no Aggregate in OpenCQRS?
The term aggregate was originally coined by DDD to refer to the write model, representing the consistency
boundary for state changes, in the context of command execution. This, however, does not imply that the aggregate needs to be
represented by a single class, e.g. Book aggregate for all book related commands. OpenCQRS lets you
choose which class is suited best for representing the write model state for a specific command. For instance, one may
choose to use the Book class for purchasing a book and a Page model class for maintaining its pages' status.
It is up to the command handler implementation to decide which write model class suits
its needs best. The only constraint is, that currently only state rebuilding handlers matching this type will be used by
OpenCQRS to reconstruct the state prior to executing the command. Apart from that, any Java class
(except java.lang.String) may be chosen; it does not even have to be serializable, as write model reconstruction
is an in-memory only operation.
Events may be consumed in two flavors in order to reconstruct the write model:
- as plain Java object representing an event's payload
- as raw
Eventif more information is required, such as itsidorhash
Raw Event Representations
Events used to reconstruct the write model will always be obtained from the event repository.
Accordingly, raw event representation in that case refers to an instance of Event, whose
data has already been upcasted, to ensure it can be mapped properly to the appropriate Java event class.
The following is an example of a Java event class representing a previously purchased book:
State Rebuilding Handler Definitions
OpenCQRS requires developers to provide StateRebuildingHandlerDefinitions,
encapsulating the state reconstruction logic. These need to be registered with a CommandRouter,
in order to be executed prior to the actual commands. StateRebuildingHandlerDefinition requires
the following parameters for its construction:
- A
java.lang.Classidentifying the type of the write model to be reconstructed. - A
java.lang.Classused to identify which type of assignable Java event objects can be applied. - A
StateRebuildingHandlerencapsulating the state reconstruction logic.
The CommandRouter upon sourcing the relevant events for the command, takes into account
all registered StateRebuildingHandlerDefinitions matching the required write model type
and the assignable event type.
State Reconstruction Constraints
Class inheritance may be leveraged to assign sourced events to StateRebuildingHandlerDefinitions
defined using a more generic Java event type, even java.lang.Object. If multiple StateRebuildingHandlerDefinitions
are capable of handling the sourced event, all of them are used to reconstruct the write model state, however, with no specific order.
State Rebuilding Handlers
There are five different types of StateRebuildingHandler that may be extended
to define the actual write model reconstruction logic; all of them are functional interfaces:
FromObjectif only the previous write model (ornull) and the Java event object is sufficient.FromObjectAndMetaDataif additional access to the event's meta-data is required.FromObjectAndMetaDataAndSubjectif additional access to the event's meta-data and subject is required.FromObjectAndMetaDataAndSubjectAndRawEventif additional access to the event's meta-data, its subject and the rawEventis required.FromObjectAndRawEventif additional access to the rawEventis required.
Accessing raw Events
Special care needs to be taken, when accessing raw Events within StateRebuildingHandlers,
since handlers will not only be called prior to command execution, but also immediately when publishing new events from within CommandHandlers
using the CommandEventPublisher. At this point in time the event has not yet been written to the
event store, so no raw information, such as the event id, is available. Hence, all raw event references will be null during that execution phase.
All five types require the developer to specify the following generic types:
- the type of the write model to be reconstructed
- the event type
Choosing a StateRebuildingHandler Type
The choice of a suitable StateRebuildingHandler is for syntactical reasons only, especially
when using Java or Kotlin lambda expressions to implement them. The choice has no effect on the reconstruction itself.
Registration
StateRebuildingHandlerDefinitions need to be created and registered with the
CommandRouter, before commands can be executed.
Manual Registration
A list of StateRebuildingHandlerDefinition can be registered with a
manually configured
CommandRouter programmatically as in the following example:
public static CommandRouter commandRouter(EventRepository eventRepository) {
StateRebuildingHandlerDefinition<Book, BookPurchasedEvent> bookPurchased = new StateRebuildingHandlerDefinition<>(
Book.class,
BookPurchasedEvent.class,
(StateRebuildingHandler.FromObject<Book, BookPurchasedEvent>) (book /* (1)! */, event) ->
new Book(
event.isbn(),
event.numPages(),
Set.of(),
null
)
);
StateRebuildingHandlerDefinition<Book, BookBorrowedEvent> bookBorrowed = new StateRebuildingHandlerDefinition<>(
Book.class,
BookBorrowedEvent.class,
(StateRebuildingHandler.FromObject<Book, BookBorrowedEvent>) (book, event) ->
new Book(
book.isbn(),
book.numPages(),
book.damagedPages(),
new Lending(event.dueAt()) /* (2)! */
)
);
return new CommandRouter(
eventRepository,
eventRepository,
List.of(/* (3)! */),
List.of(bookPurchased, bookBorrowed)
);
}
- Previous
bookstate isn't used in this example, asBookPurchasedEventrepresents the initial event, therefore the state isnull. - Previous
bookstate is updated by creating a copy includingdueAttimestamp. CommandHandlerDefinitions omitted, see
Spring Boot based Registration
When using the Spring Boot auto-configured command router
StateRebuildingHandlerDefinitions 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 StateRebuildingHandlerDefinition 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.
State Rebuilding Handler Definition using @Bean Methods
StateRebuildingHandlerDefinitions can be defined using org.springframework.context.annotation.Bean
annotated methods, as shown in the following example.
@CommandHandlerConfiguration
public class BookHandlers {
@Bean
public StateRebuildingHandlerDefinition<Book, BookPurchasedEvent> bookPurchased(/* (1)! */) {
return new StateRebuildingHandlerDefinition<>(
Book.class,
BookPurchasedEvent.class,
(StateRebuildingHandler.FromObject<Book, BookPurchasedEvent>) (book, event) ->
new Book(
event.isbn(),
event.numPages(),
Set.of(),
null
)
);
}
}
- The
StateRebuildingHandlermay access additional auto-wired beans, which can be defined as auto-wired dependencies within the@Beanannotated method signature, though this is rarely needed.
State Rebuilding Handler Definition using Annotations
StateRebuildingHandlerDefinitions may be defined - even in combination with @Bean definitions -
using the StateRebuilding annotation on any suitable state rebuilding method. This greatly
simplifies the definition, since developers are relieved from explicitly instantiating StateRebuildingHandlerDefinition
and choosing a proper StateRebuildingHandler subtype, as shown in the following example:
@CommandHandlerConfiguration
public class BookHandlers {
@StateRebuilding
public Book on(BookPurchasedEvent event /* (1)! */) {
return new Book(
event.isbn(),
event.numPages(),
Set.of(),
null
);
}
}
- No previous
bookstate is injected in this example, asBookPurchasedEventrepresents the initial event, therefore the state isnull.
The following rules apply with respect to StateRebuilding annotated definitions,
enforced by StateRebuildingAnnotationProcessingAutoConfiguration 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 must return a valid write model type inheriting
java.lang.Object. - 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 the Java object event type, which must differ from the returned write model type.
- An optional parameter representing the previous write model state (or
null) may be defined. - An optional parameter of type
java.util.Map<String, ?>may be defined to access the event meta-data. - An optional parameter of type
java.lang.Stringmay be defined to access the event subject. - An optional nullable parameter of type
Eventmay be defined to access the raw event. - 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 state reconstruction or event publication. 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.