Skip to content

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.

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:

@Aggregate
public record ShopItem(@EntityId ItemId itemId,
ItemDetails details) {
}

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) {
}

A document-based entity can still use:

  • @InterceptApply to block or modify updates
  • @AssertLegal to validate updates
  • @Apply to 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).


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) {
}

This hybrid approach is ideal when you need both traceability and query speed.

You can further customize the persistence behavior of aggregates using settings in @Aggregate:

  • eventPublication: prevent events when nothing has changed
  • publicationStrategy: store-only vs publish-and-store
  • snapshotPeriod: replace aggregate snapshot after every N updates
  • searchable: 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();
}
}

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 cache
  • cachingDepth: 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.


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:

@HandleCommand
void handle(DualUpdate command) {
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.


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().


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