Annotation Interface Stateful
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 (viaAssociation
), 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 aHandlerRepository
.
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 returnsnull
, 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:
collection()
defines the search collection nametimestampPath()
andendPath()
define time bounds for filtering
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 ElementsModifier and TypeOptional ElementDescriptionName 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 collectionName of the collection in which the stateful handler instance will be stored.Defaults to the simple class name (e.g.,
PaymentProcess
→paymentProcess
).- See Also:
- Default:
""
-
timestampPath
String timestampPathPath 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 endPathOptional 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 commitInBatchDetermines 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 whencommitInBatch
istrue
, 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
-