Annotation Interface Stateful


@Documented @Target(TYPE) @Retention(RUNTIME) @Inherited @Searchable @Component @Scope("prototype") public @interface Stateful
Declares that a class is a stateful message handler — i.e., one whose state is persisted and which can receive messages via Association.

Stateful handlers are used to model long-lived processes or external interactions (e.g. async API flows, payments, inboxes). These handlers are typically stored and restored across messages and retain their internal state.

Handler Activation

When a message matches one or more handlers (via Association), all matching handlers are:
  • Loaded from storage (usually the search index)
  • And invoked via matching @Handle... methods.

A single message may invoke multiple matching stateful handlers.

Persistence

Handler state is persisted via a HandlerRepository. By default, the DocumentStore is used.
  • The identifier of a handler is derived from an EntityId property or auto-generated if absent.
  • Handlers are immutable by convention and updated using withX(...) methods.

Basic Example


 @Value
 @Stateful
 public class PaymentProcess {
     @EntityId String id;
     @Association String pspReference;
     PaymentStatus status;

     @HandleEvent
     static PaymentProcess on(PaymentInitiated event) {
         String pspReference = FluxCapacitor.sendCommandAndWait(new ExecutePayment(...));
         return new PaymentProcess(event.getPaymentId(), pspReference, PaymentStatus.PENDING);
     }

     @HandleEvent
     PaymentProcess on(PaymentConfirmed event) {
         return withStatus(PaymentStatus.CONFIRMED);
     }
 }
 

Deleting a stateful handler

If a handler method returns null, the instance is removed from its persistence store. This can be useful for modeling short-lived processes or sagas that end upon receiving a terminal event.

 @HandleEvent
 PaymentProcess on(PaymentFailed event) {
     return null; // Delete this handler
 }
 

Type constraints on handler updates

State changes to a @Stateful handler are only applied if the method returns a value that is assignable to the handler's class type. For example:
  • If the method returns void, no update occurs.
  • If the method returns a value that is unrelated to the handler type, the return value is ignored for state updates.

This makes it safe to return primitive types or utility values from handler methods without risk of overwriting the handler state. For instance:


 @HandleSchedule
 Duration on(CheckPaymentStatus schedule) {
     PaymentStatus status = FluxCapacitor.queryAndWait(new CheckStatus(schedule.getPaymentId()));
     if (status == COMPLETED) {
         return null; // stop scheduling
     }
     return Duration.ofMinutes(1); // retry in 1 minute
 }
 

In this example, returning a Duration controls the rescheduling behavior, but it does not affect the stored state of the PaymentProcess handler.

Search Indexing

Like aggregates, @Stateful handlers are also Searchable and can be indexed automatically:

Tracking Isolation

@Stateful handlers may optionally include a Consumer annotation to define their own tracking configuration (e.g. isolation, concurrency, retry policy).
See Also:
  • Optional Element Summary

    Optional Elements
    Modifier and Type
    Optional Element
    Description
    Name of the collection in which the stateful handler instance will be stored.
    boolean
    Determines whether the state changes to this handler should be committed at the end of the current message batch (if applicable), or immediately after the message that triggered the change (default behavior).
    Optional path to extract an end timestamp for search indexing.
    Path to extract the main timestamp used in search indexing.
  • Element Details

    • collection

      String collection
      Name of the collection in which the stateful handler instance will be stored.

      Defaults to the simple class name (e.g., PaymentProcesspaymentProcess).

      See Also:
      Default:
      ""
    • timestampPath

      String timestampPath
      Path to extract the main timestamp used in search indexing.

      If endPath() is not specified, this will be used as both start and end time.

      Useful for time-based search queries (e.g., validity or activity windows).

      See Also:
      Default:
      ""
    • endPath

      String endPath
      Optional path to extract an end timestamp for search indexing.

      If omitted, the start timestamp will also be used as the end timestamp.

      See Also:
      Default:
      ""
    • commitInBatch

      boolean commitInBatch
      Determines whether the state changes to this handler should be committed at the end of the current message batch (if applicable), or immediately after the message that triggered the change (default behavior).

      If set to true, changes are deferred and committed once all messages in the current batch have been processed. This is particularly useful for reducing round-trips to the underlying persistence store when applying lots of updates.

      Association behavior with deferred commits

      Even though the state is not yet persisted when commitInBatch is true, the message routing logic remains accurate and consistent. This is achieved by maintaining a local cache of uncommitted changes.

      The cache is used to:

      • Ensure that newly created handlers can be matched to later messages in the same batch.
      • Prevent messages from being routed to handlers that have been deleted earlier in the batch.
      • Use the most recent (updated) state when evaluating associations for subsequent messages.

      In other words, association lookups during batch processing always take into account:

      • The persisted state (from the backing repository), and
      • The in-memory cache of batch-local updates (created, updated, or deleted handlers).

      This guarantees consistent and predictable behavior within the boundaries of the current batch, even when the persistent state has not yet been flushed.

      Note:

      If no current batch is active (e.g. in synchronous or local usage), changes are always committed immediately, regardless of this setting.
      Default:
      false