Skip to content

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

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:

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

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:

  1. A java.lang.Class identifying the type of the write model to be reconstructed.
  2. A java.lang.Class used to identify which type of assignable Java event objects can be applied.
  3. A StateRebuildingHandler encapsulating 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:

  1. FromObject if only the previous write model (or null) and the Java event object is sufficient.
  2. FromObjectAndMetaData if additional access to the event's meta-data is required.
  3. FromObjectAndMetaDataAndSubject if additional access to the event's meta-data and subject is required.
  4. FromObjectAndMetaDataAndSubjectAndRawEvent if additional access to the event's meta-data, its subject and the raw Event is required.
  5. FromObjectAndRawEvent if additional access to the raw Event is 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:

  1. the type of the write model to be reconstructed
  2. 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)
      );
    }
  1. Previous book state isn't used in this example, as BookPurchasedEvent represents the initial event, therefore the state is null.
  2. Previous book state is updated by creating a copy including dueAt timestamp.
  3. 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
                            )
        );
      }
}
  1. The StateRebuildingHandler 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.

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
            );
      }
}
  1. No previous book state is injected in this example, as BookPurchasedEvent represents the initial event, therefore the state is null.

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.String may be defined to access the event subject.
  • An optional nullable parameter of type Event may be defined to access the raw event.
  • Any number of @Autowired annotated 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.