Snapshots

The SnapshotStore trait and the Snapshotting decorator — and why Closing the Books is usually the better tool.

Snapshots

Rehydrating an aggregate means replaying its whole stream. For a long stream that gets slow, and a snapshot is the usual fix: periodically save the folded state plus the position it was folded to, and on load start from there instead of from event one.

mnesis supports snapshots — but first, a strong recommendation.

Prefer Closing the Books to snapshots whenever your domain has a natural cycle. A summary event flows through the same fold and the same upcaster pipeline as any event; a snapshot is opaque bytes with no migration path. Reach for a snapshot only when you genuinely cannot bound the stream.

The SnapshotStore trait

One trait powers both aggregate snapshots and projection state:

rust
pub trait SnapshotStore<S, P>: Send + Sync {
    async fn hydrate(&self, id, schema_version) -> Result<Hydrated<S, P>, Self::Error>;
    async fn commit(&self, id, schema_version, position: P, state: &S) -> Result&lt;(), Self::Error>;
}

State and position are saved and loaded together, never separately. That is the key safety property: a persisted checkpoint can never be ahead of the state it describes, so a lost write costs a re-fold (idempotent), never a skipped event. It is a structural guarantee — it does not depend on fsync timing.

P is the position type: Version for a single aggregate stream, GlobalSeq for a multi-stream projection.

Hydrated — three states, not Option

hydrate returns a trichotomy, not Option:

rust
pub enum Hydrated<S, P> {
    Absent,                              // nothing saved — start empty / replay
    Stale { stored_schema },             // saved under a *different* schema — must rebuild
    Found { position, state },           // usable checkpoint
}

Why the extra state matters: for an aggregate snapshot, Absent and Stale both just mean "replay the stream," and the decorator collapses them. But for a projection, Stale means a schema bump invalidated the saved state and the next step is a full re-fold of the whole $all stream — on a mobile/IoT host, a long, battery-heavy operation the host must be able to see coming. Stale deliberately carries no stale bytes: derived state has no upcasting path, so a schema change forces a rebuild, never a migration.

Aggregate snapshots — the Snapshotting decorator

You never call SnapshotStore by hand for an aggregate. Configure it on the repository builder and it becomes transparent:

rust
let repo = store
    .repository::<BankAccount>()
    .json()
    .snapshot_store_json(snapshot_store)   // wrap in Snapshotting
    .build();

Now load hydrates from the snapshot (falling back to replay if Absent/Stale) and save commits a fresh snapshot per a PersistTrigger. Snapshot writes are best-effort — they never block event persistence; a failed snapshot just means the next load re-folds.

When to persist — PersistTrigger

  • EveryNEvents(n) — snapshot each time the version crosses a bucket of n.
  • AfterEventTypes(&[..]) — snapshot after semantically meaningful events.

The bridge to byte stores

Adapters implement SnapshotStore<Vec<u8>, P> (raw bytes). CodecSnapshotStore bridges that to a typed SnapshotStore<S, P> using your Encode/Decode, so the same codec serializes both events and snapshots.

Where to next

  • Closing the Books — the preferred alternative.
  • Repository — wiring snapshots in.
  • CQRS — projections use the same SnapshotStore for their checkpoint.