Model persistence
Fluxzero supports multiple strategies for loading and storing aggregates:
- Event sourcing: state is derived by replaying a stream of applied updates (events)
- Document storage: the full aggregate is stored as a document
- In-memory only: ephemeral state, not persisted across messages
By default, aggregates use event sourcing (@Aggregate(eventSourced = true)), but you can configure each aggregate individually.
Event-sourcing
Section titled “Event-sourcing”By default, Fluxzero stores aggregates as a series of updates (events) that were applied using @Apply methods.
When an event-sourced aggregate is loaded, Fluxzero replays applied updates to rebuild current state. During this process, each event is re-applied to the aggregate. See Entity loading for more information.
For remote Runtimes, the SDK pages long histories by event count. Applications using
fluxzero.defaults.version >= 2026.09.10 also request a 100 MiB serialized payload limit per page; configure
fluxzero.eventsourcing.maxFetchBytes explicitly to tune it or use 0 for count-only compatibility behavior. A single
larger event is allowed through to guarantee progress, and later pages resume at the exact next aggregate sequence.
Runtimes predating this optional request field ignore it and continue to return count-only pages.
Event-sourcing will be suitable for most of your aggregates, especially your core domain objects and business processes.
As event-sourcing is the default for aggregates it is enough to mark your aggregate class with @Aggregate:
@Aggregatepublic record ShopItem(@EntityId ItemId itemId, ItemDetails details) {}@Aggregatedata class ShopItem( @EntityId val itemId: ItemId, val details: ItemDetails)Document storage
Section titled “Document storage”Fluxzero can also store aggregates as documents in a searchable document store. This is useful for:
- Read-heavy aggregates
- Aggregates with large histories
- Reference models that don’t need event streams
To enable document storage, set searchable = true in the @Aggregate annotation:
@Aggregate(eventSourced = false, searchable = true, collection = "countries")public record Country(@EntityId String countryCode, String name) {}@Aggregate(eventSourced = false, searchable = true, collection = "countries")data class Country( @EntityId val countryCode: String, val name: String)A document-based entity can still use:
@InterceptApplyto block or modify updates@AssertLegalto validate updates@Applyto compute and update state
Each applied update overwrites the document in the store and, by default, is also stored and published as an event. If you want to disable event publication, use @Aggregate(eventPublication = NEVER).
Dual persistence
Section titled “Dual persistence”You can combine both strategies by enabling eventSourced = true and searchable = true.
Fluxzero will:
- Store events for replay and auditing
- Index the latest version as a document for fast retrieval and search
@Aggregate(searchable = true)public record Order(@EntityId OrderId orderId, OrderDetails details) {}@Aggregate(searchable = true)data class Order( @EntityId val orderId: OrderId, val details: OrderDetails)This hybrid approach is ideal when you need both traceability and query speed.
Persistence behavior
Section titled “Persistence behavior”You can further customize the persistence behavior of aggregates using settings in @Aggregate:
eventPublication: prevent events when nothing has changedpublicationStrategy: store-only vs publish-and-storesnapshotPeriod: replace aggregate snapshot after every N updatessearchable: store aggregate in document store after each commit
@Aggregate(snapshotPeriod = 1000)public record UserAccount(@EntityId UserId userId, UserProfile profile) {
@Apply UserAccount apply(UpdateProfile update) { return toBuilder().profile(update.getProfile()).build(); }}@Aggregate(snapshotPeriod = 1000)data class UserAccount( @EntityId val userId: UserId, val profile: UserProfile) { @Apply fun apply(update: UpdateProfile): UserAccount { return copy(profile = update.profile) }}Caching and checkpoints
Section titled “Caching and checkpoints”Fluxzero automatically caches aggregates after loading or applying updates unless @Aggregate(cached = false). This enables:
- Fast reuse of recently loaded aggregates
- Automatic rehydration from snapshots or partial checkpoints (when configured)
You can tune cache behavior with:
cached: disable shared cachecachingDepth: how many versions to retain (enables.previous()access)checkpointPeriod: how often to insert intermediate checkpoints
Aggregates of a given type can also be configured to use their own dedicated cache with FluxzeroBuilder#withAggregateCache(...).
See Configuring Fluxzero for more details.
Commits and rollbacks
Section titled “Commits and rollbacks”Once an aggregate is updated successfully and no errors occur during message handling, its changes are eventually committed.
However, if a handler fails, all updates performed within that handler are rolled back automatically:
@HandleCommandvoid handle(DualUpdate command) { Fluxzero.loadAggregate(command.firstId()) .assertAndApply(command.firstUpdate()); // succeeds Fluxzero.loadAggregate(command.secondId()) .assertAndApply(command.secondUpdate()); // fails}@HandleCommandfun handle(command: DualUpdate) { Fluxzero.loadAggregate(command.firstId()) .assertAndApply(command.firstUpdate()) // succeeds Fluxzero.loadAggregate(command.secondId()) .assertAndApply(command.secondUpdate()) // fails}In this example, both updates are rolled back because the second apply fails — ensuring atomic consistency across aggregates.
Commit timing
Section titled “Commit timing”By default, updates are committed only after the currently tracked message batch completes, not immediately. This means:
- Updates are locally cached (per tracker thread) until the batch is processed.
- Unnecessary round-trips to the Fluxzero runtime are avoided during batch processing.
You can change this behavior by setting @Aggregate(commitInBatch = false), which will commit changes at the end of
the current message instead. You can even commit manually at any time using Entity#commit().
Commit process
Section titled “Commit process”When an aggregate is committed, Fluxzero processes each updated aggregate individually through the following steps:
graph TD
COMMIT[Commit changes] --> STRATEGY{Publication strategy}
STRATEGY -->|Store-only| STORE[Append update to event store]
STRATEGY -->|Publish-and-store| STORE_PUB[Append update & publish to global event log]
STRATEGY -->|Publish-only| PUB_ONLY[Publish without appending to store]
STORE --> CACHE{Is caching enabled?}
STORE_PUB --> CACHE
PUB_ONLY --> CACHE
CACHE -->|Yes| UPDATE_CACHE[Write updated state to cache]
CACHE -->|No| SNAPSHOT{Snapshot period reached?}
UPDATE_CACHE --> SNAPSHOT
SNAPSHOT -->|Yes| CREATE_SNAPSHOT[Write new snapshot]
SNAPSHOT -->|No| SEARCHABLE{Aggregate searchable?}
CREATE_SNAPSHOT --> SEARCHABLE
SEARCHABLE -->|Yes| STORE_DOC[Store aggregate snapshot in document store]
SEARCHABLE -->|No| DONE[Commit finished]
STORE_DOC --> DONE © 2026 Fluxzero