Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Snapshots

Replaying events gets expensive as aggregates accumulate history. Snapshots checkpoint aggregate state, allowing you to skip replaying old events.

How Snapshots Work

ApplicationSnapshotStoreEventStoreAggregate  load("account", "ACC-001")  Snapshot at position 1000Deserialize snapshotload_events(after: 1000)Events 1001-1050apply(event) for each








Instead of replaying all 1050 events, load the snapshot and replay only the 50 new ones.

Enabling Snapshots

Use with_snapshots() when creating the repository:

use sourcery::{Repository, snapshot::inmemory, store::inmemory as event_store};

let event_store = event_store::Store::new();
let snapshot_store = inmemory::Store::every(100);

let repository = Repository::new(event_store)
    .with_snapshots(snapshot_store);

Snapshot Policies

snapshot::inmemory::Store provides three policies:

Always

Save a snapshot after every command:

let snapshots = inmemory::Store::always();

Use for: Aggregates with expensive replay, testing.

Every N Events

Save after accumulating N events since the last snapshot:

let snapshots = inmemory::Store::every(100);

Use for: Production workloads balancing storage vs. replay cost.

Never

Never save (load-only mode):

let snapshots = inmemory::Store::never();

Use for: Read-only replicas, debugging.

The SnapshotStore Trait

pub trait SnapshotStore<Id: Sync>: Send + Sync {
    /// Position type for tracking snapshot positions.
    ///
    /// Must match the `EventStore::Position` type used in the same repository.
    type Position: Send + Sync;

    /// Error type for snapshot operations.
    type Error: std::error::Error + Send + Sync + 'static;

    /// Load the most recent snapshot for an aggregate.
    ///
    /// Returns `Ok(None)` if no snapshot exists.
    ///
    /// # Errors
    ///
    /// Returns an error if the underlying storage fails.
    fn load<T>(
        &self,
        kind: &str,
        id: &Id,
    ) -> impl std::future::Future<Output = Result<Option<Snapshot<Self::Position, T>>, Self::Error>> + Send
    where
        T: DeserializeOwned;

    /// Whether to store a snapshot, with lazy snapshot creation.
    ///
    /// The repository calls this after successfully appending new events,
    /// passing `events_since_last_snapshot` and a `create_snapshot`
    /// callback. Implementations may decline without invoking
    /// `create_snapshot`, avoiding unnecessary snapshot creation cost
    /// (serialisation, extra I/O, etc.).
    ///
    /// Returning [`SnapshotOffer::Stored`] indicates that the snapshot was
    /// persisted. Returning [`SnapshotOffer::Declined`] indicates that no
    /// snapshot was stored.
    ///
    /// # Errors
    ///
    /// Returns [`OfferSnapshotError::Create`] if `create_snapshot` fails.
    /// Returns [`OfferSnapshotError::Snapshot`] if persistence fails.
    fn offer_snapshot<CE, T, Create>(
        &self,
        kind: &str,
        id: &Id,
        events_since_last_snapshot: u64,
        create_snapshot: Create,
    ) -> impl std::future::Future<
        Output = Result<SnapshotOffer, OfferSnapshotError<Self::Error, CE>>,
    > + Send
    where
        CE: std::error::Error + Send + Sync + 'static,
        T: Serialize,
        Create: FnOnce() -> Result<Snapshot<Self::Position, T>, CE> + Send;
}

The repository calls offer_snapshot after successfully appending new events. Implementations may decline without invoking create_snapshot, avoiding unnecessary snapshot serialisation.

The Snapshot Type

pub struct Snapshot<Pos, Data> {
    pub position: Pos,
    pub data: Data,
}

The position indicates which event this snapshot was taken after. When loading, only events after this position need to be replayed.

Projection Snapshots

Projection snapshots use the same store and are keyed by (P::KIND, instance_id). Use load_projection_with_snapshot on a repository configured with snapshots:

#[derive(Default, Serialize, Deserialize, sourcery::Projection)]
#[projection(kind = "loyalty.summary")]
struct LoyaltySummary {
    total_earned: u64,
}

// (ProjectionFilters impl defines filters and associated types)

let repo = Repository::new(store).with_snapshots(inmemory::Store::every(100));

let summary: LoyaltySummary = repo
    .load_projection_with_snapshot::<LoyaltySummary>(&instance_id)
    .await?;

Singleton projections use InstanceId = () and pass &(). Instance projections pass their instance identifier.

Projection snapshots require a globally ordered store (S: GloballyOrderedStore) and P: Serialize + DeserializeOwned.

When to Snapshot

Aggregate TypeRecommendation
Short-lived (< 100 events)Skip snapshots
Medium (100-1000 events)Every 100-500 events
Long-lived (1000+ events)Every 100 events
High-throughputEvery N events, tuned to your SLA

Implementing a Custom Store

For production, implement SnapshotStore with your database. See Custom Stores for a complete guide.

Snapshot Invalidation

Snapshots are tied to your aggregate’s serialised form. When you change the struct:

  1. Add fields — Use #[serde(default)] for backwards compatibility
  2. Remove fields — Old snapshots still deserialise (extra fields ignored)
  3. Rename fields — Use #[serde(alias = "old_name")]
  4. Change types — Old snapshots become invalid; delete them

For major changes, delete old snapshots and let them rebuild from events.

Next

Event Versioning — Evolving event schemas over time