Skip to content

Persistence

datum-persistence adds actor-oriented event sourcing, durable state, storage backends, CQRS queries, and managed read-side projections to Datum. Persistent behaviors and projections are side-effect-free blueprints: constructing them does not touch storage or start an actor. Recovery begins only when you call spawn or run_on.

Choose the state model

ModelDurable recordRecoveryBest fit
Event sourcingEvery accepted domain event, ordered by SequenceNr within one PersistenceIdLoad an optional snapshot, then replay the journal tailAuditability, temporal reasoning, projections, and aggregates whose decisions matter
Durable stateOnly the latest serialized state or tombstone, guarded by a CAS RevisionOne store readLatest-value workflows where the event history is not part of the domain contract

Both models serialize explicitly through a SerializerCatalog. Event-sourced behaviors also use an EventAdapter; snapshots and durable state use a StateAdapter. Stable serializer ids, serializer manifests, and adapter manifests are durable protocol, not incidental implementation details.

Event-sourced write path

For one command, an EventSourcedBehavior:

  1. Calls the command handler with the last confirmed state.
  2. Resolves its Effect, including async_effect, into zero or more domain events.
  3. Applies every event to a staged state and serializes the whole batch. Invalid transitions or serialization stop before journal I/O.
  4. Appends the batch as one AtomicWrite. persist_all is therefore all-or-nothing and gapless.
  5. Installs the staged state only after the journal confirms the append.
  6. Saves a requested snapshot, publishes confirmed domain events to the handle's Topic, and runs chained side effects and replies in registration order.

Commands and EventSourcedHandle::state queries share one actor mailbox. A state query observes the confirmed state after messages enqueued before it; it never exposes staged state while a write is in flight.

Event-sourced recovery path

With the default Recovery::Enabled(SnapshotSelection::Latest), spawning:

  1. Loads the selected snapshot when a snapshot store is configured.
  2. Decodes it through the state catalog and adapter, installs its state and sequence number, and emits EventSourcedSignal::SnapshotRecovered.
  3. Reads the journal high watermark and replays retained events after the snapshot.
  4. Decodes serializer frames first, then applies the event adapter's recovery-side 0..n mapping.
  5. Emits EventSourcedSignal::RecoveryCompleted, including for a new persistence id.

Recovery does not rerun command-handler effects, replies, event publishing, or then_run callbacks. Recovery::Disabled skips state restoration but still reads the journal watermark so later sequence numbers remain safe.

Durable-state write and recovery paths

A DurableStateBehavior recovers with one DurableStateStore::get. An absent id starts at revision zero. Effect::persist writes the next revision with compare-and-set semantics; Effect::delete advances the revision and leaves a tombstone, then installs the behavior's empty state in memory. The revision is monotonic across updates, deletes, and recovery.

When a ChangeEventHandler is configured, the durable CAS happens first and the derived journal event is appended second. Those are deliberately two separate writes: there is no cross-store atomicity guarantee, and side effects run only after both succeed.

Rejection is not failure

The distinction determines whether retry is safe.

ResultMeaningEvent-sourced behavior
AppendOutcome::Rejected { reason } or PersistenceError::Rejected before I/ODeterministic refusal: the write is known not to have committedEmits PersistRejected and stops. Persist-failure backoff does not retry it.
Err(PersistenceError::Failure(_)) / Connection(_) from storageStorage failed or the commit result may be uncertainEmits PersistFailed; stops by default, or recovers after on_persist_failure backoff.
PersistenceError::Serialization(_)A durable frame could not be encoded or decodedA write-side encoding error is rejected before append; a recovery decode error fails recovery.
PersistenceError::RevisionConflict { .. }Durable-state CAS used a stale or skipped revisionEnters the durable behavior failure path; refresh state before retrying direct store operations.

Never turn an uncertain storage failure into a deterministic rejection. Conversely, do not retry a known rejection with backoff: it will remain invalid until the command or schema changes.

Guides

  • Event sourcing — behaviors, effects, replies, signals, snapshots, taggers, and confirmed-event publishing.
  • Durable state — CAS revisions, tombstones, and change events.
  • Backends — memory/testkit, PostgreSQL, and RocksDB setup.
  • Read side — query capabilities, offsets, and Datum Source integration.
  • Projections — managed delivery semantics, durable cursors, recovery, management, slices, local scale-out, and the projection testkit.
  • Schema evolution — catalogs, manifests, adapters, and committed compatibility fixtures.
  • Persistence cookbook recipes — focused patterns ready to adapt.