Skip to content

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 Event if more information is required, such as its id or hash

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:

public record BookPurchasedEvent(
        String isbn, 
        String author, 
        String title, 
        long numPages
) {}

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:

  1. A java.lang.String identifying the processing group. Only definitions of the same group may be registered with the same EventHandlingProcessor instance.
  2. A java.lang.Class used to identify which type of assignable Java event objects can be applied.
  3. An EventHandler encapsulating 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:

  1. ForObject if only the Java event object is sufficient.
  2. ForObjectAndMetaData if additional access to the event's meta-data is required.
  3. ForObjectAndMetaDataAndRawEvent if additional access to the event's meta-data ,and the raw Event is 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
    );
}
  1. 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());
                  }
            );
      }
}
  1. The EventHandlerDefinition may access additional auto-wired beans, which can be defined as auto-wired dependencies within the @Bean annotated 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:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EventHandling("book-catalog")
public @interface BookCatalogHandler {}

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 @Autowired annotated parameters may be defined for injection of dependent Spring beans.
  • The method may optionally be annotated with Spring's @Transactional annotation 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.