Appearance
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
| Model | Durable record | Recovery | Best fit |
|---|---|---|---|
| Event sourcing | Every accepted domain event, ordered by SequenceNr within one PersistenceId | Load an optional snapshot, then replay the journal tail | Auditability, temporal reasoning, projections, and aggregates whose decisions matter |
| Durable state | Only the latest serialized state or tombstone, guarded by a CAS Revision | One store read | Latest-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:
- Calls the command handler with the last confirmed state.
- Resolves its
Effect, includingasync_effect, into zero or more domain events. - Applies every event to a staged state and serializes the whole batch. Invalid transitions or serialization stop before journal I/O.
- Appends the batch as one
AtomicWrite.persist_allis therefore all-or-nothing and gapless. - Installs the staged state only after the journal confirms the append.
- 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:
- Loads the selected snapshot when a snapshot store is configured.
- Decodes it through the state catalog and adapter, installs its state and sequence number, and emits
EventSourcedSignal::SnapshotRecovered. - Reads the journal high watermark and replays retained events after the snapshot.
- Decodes serializer frames first, then applies the event adapter's recovery-side
0..nmapping. - 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.
| Result | Meaning | Event-sourced behavior |
|---|---|---|
AppendOutcome::Rejected { reason } or PersistenceError::Rejected before I/O | Deterministic refusal: the write is known not to have committed | Emits PersistRejected and stops. Persist-failure backoff does not retry it. |
Err(PersistenceError::Failure(_)) / Connection(_) from storage | Storage failed or the commit result may be uncertain | Emits PersistFailed; stops by default, or recovers after on_persist_failure backoff. |
PersistenceError::Serialization(_) | A durable frame could not be encoded or decoded | A write-side encoding error is rejected before append; a recovery decode error fails recovery. |
PersistenceError::RevisionConflict { .. } | Durable-state CAS used a stale or skipped revision | Enters 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
Sourceintegration. - 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.