Defining Event Handlers
Within CQRS applications event handling logic is responsible for
projecting read models
in order for clients to be able to query the application state. The event handling processor
is responsible for executing all relevant EventHandlerDefinitions
based on the tracked events.
Events and Read Models
Events represent facts or state changes within a CQRS/ES application. Accordingly, they can be used to project read models. A read model may be anything ranging from SQL database projections to emails sent to customers. OpenCQRS makes no assumptions about read models, but focuses on delivering events to the appropriate event handlers.
Events may be consumed in two flavors in order to project read models:
- 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 project read models 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:
Event Handler Definitions
OpenCQRS requires developers to provide EventHandlerDefinitions,
encapsulating the event processing logic. These need to be registered with a EventHandlingProcessor,
in order to be processed asynchronously. EventHandlerDefinition requires
the following parameters for its construction:
- A
java.lang.Stringidentifying the processing group. Only definitions of the same group may be registered with the sameEventHandlingProcessorinstance. - A
java.lang.Classused to identify which type of assignable Java event objects can be applied. - An
EventHandlerencapsulating the event processing logic.
The EventHandlingProcessor within its
event processing loop, takes into account
all registered EventHandlerDefinitions matching the assignable event type.
Event Handling Constraints
Class inheritance may be leveraged to assign events to EventHandlerDefinitions
defined using a more generic Java event type, even java.lang.Object. If multiple EventHandlerDefinitions
are capable of handling the event, all of them are used to project the read model, however, with no specific order.
Event Handlers
There are three different types of EventHandler that may be extended
to define the actual event processing logic; all of them are functional interfaces:
ForObjectif only the Java event object is sufficient.ForObjectAndMetaDataif additional access to the event's meta-data is required.ForObjectAndMetaDataAndRawEventif additional access to the event's meta-data ,and the rawEventis required.
All three types require the developer to specify the event type as generic.
Choosing an EventHandler Type
The choice of a suitable EventHandler is for syntactical reasons only, especially
when using Java or Kotlin lambda expressions to implement them. The choice has no effect on the event processing itself.
Registration
All EventHandlerDefinitions within the same processing group
need to be created and registered with an instance of EventHandlingProcessor,
before entering the event processing loop.
Manual Registration
A list of EventHandlerDefinition can be registered with a
manually configured
EventHandlingProcessor programmatically as in the following example:
public static EventHandlingProcessor eventHandlingProcessor(
EventReader eventReader,
BookStore store
) {
EventHandlerDefinition<BookPurchasedEvent> bookPurchased = new EventHandlerDefinition<>(
"book-catalog",
BookPurchasedEvent.class,
(EventHandler.ForObject<BookPurchasedEvent>) (event) -> {
store.register(event.isbn()); /* (1)! */
}
);
return new EventHandlingProcessor(
0,
"/",
true,
eventReader,
new InMemoryProgressTracker(),
new PerSubjectEventSequenceResolver(),
new DefaultPartitionKeyResolver(1),
List.of(bookPurchased),
() -> () -> 1000
);
}
- Example representing a persistent book store (read model), which is being updated by the event handler.
Spring Boot based Registration
When using the Spring Boot auto-configured event handling processors
EventHandlerDefinitions can be defined as Spring beans, in order to be
auto-wired with a processing group specific EventHandlingProcessor instance created by
the EventHandlingProcessorAutoConfiguration.
Event Handler Definition using @Bean Methods
EventHandlerDefinitions can be defined using org.springframework.context.annotation.Bean
annotated methods, as shown in the following example.
@Configuration
public class BookStoreProjector {
@Bean
public EventHandlerDefinition<BookPurchasedEvent> bookPurchased(BookStore store /* (1)! */) {
return new EventHandlerDefinition<>(
"book-catalog",
BookPurchasedEvent.class,
(EventHandler.ForObject<BookPurchasedEvent>) (event) -> {
store.register(event.isbn());
}
);
}
}
- The
EventHandlerDefinitionmay access additional auto-wired beans, which can be defined as auto-wired dependencies within the@Beanannotated method signature, though this is rarely needed.
Event Handler Definition using Annotations
EventHandlerDefinitions may be defined - even in combination with @Bean definitions -
using the EventHandling annotation on any suitable event handling method. This greatly
simplifies the definition, since developers are relieved from explicitly instantiating EventHandlerDefinition
and choosing a proper EventHandler subtype, as shown in the following example:
@Component
public class BookStoreProjector {
@EventHandling("book-catalog")
public void on(BookPurchasedEvent event, @Autowired BookStore store) {
store.register(event.isbn());
}
}
Avoiding redundant Processing Group Definitions
EventHandling requires a processing group identifier to assign the
annotated method to the correct EventHandlingProcessor instance for processing.
In order to avoid redundant definitions of the group identifier, custom meta-annotations may be used instead, e.g. as follows:
The following rules apply with respect to EventHandling annotated definitions,
enforced by EventHandlingAnnotationProcessingAutoConfiguration 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
void. - The method arguments can be ordered arbitrarily, as needed.
- The method arguments must be non-repeatable and un-ambiguous.
- The method must have at least one (or any combination) of the following parameters:
- the Java object event type
- a
java.util.Map<String, ?>containing the event meta-data - a raw
Event
- Any number of
@Autowiredannotated parameters may be defined for injection of dependent Spring beans. - The method may optionally be annotated with Spring's
@Transactionalannotation to enforce transaction semantics for the method execution.
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.