Snapshots
Replaying events gets expensive as aggregates accumulate history. Snapshots checkpoint aggregate state, allowing you to skip replaying old events.
How Snapshots Work
Instead of replaying 1050 events, you load the snapshot and replay only 50.
Enabling Snapshots
Use with_snapshots() when creating the repository:
use sourcery::{Repository, snapshot::InMemorySnapshotStore, store::{inmemory, JsonCodec}};
let event_store = inmemory::Store::new(JsonCodec);
let snapshot_store = InMemorySnapshotStore::always();
let mut repository = Repository::new(event_store)
.with_snapshots(snapshot_store);
Snapshot Policies
InMemorySnapshotStore provides three policies:
Always
Save a snapshot after every command:
let snapshots = InMemorySnapshotStore::always();
Use for: Aggregates with expensive replay, testing.
Every N Events
Save after accumulating N events since the last snapshot:
let snapshots = InMemorySnapshotStore::every(100);
Use for: Production workloads balancing storage vs. replay cost.
Never
Never save (load-only mode):
let snapshots = InMemorySnapshotStore::never();
Use for: Read-only replicas, debugging.
The SnapshotStore Trait
pub trait SnapshotStore: Send + Sync {
/// Aggregate identifier type.
///
/// Must match the `EventStore::Id` type used in the same repository.
type Id: Send + Sync + 'static;
/// Position type for tracking snapshot positions.
///
/// Must match the `EventStore::Position` type used in the same repository.
type Position: Send + Sync + 'static;
/// 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<'a>(
&'a self,
aggregate_kind: &'a str,
aggregate_id: &'a Self::Id,
) -> impl Future<Output = Result<Option<Snapshot<Self::Position>>, Self::Error>> + Send + 'a;
/// 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
/// (serialization, 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<'a, CE, Create>(
&'a mut self,
aggregate_kind: &'a str,
aggregate_id: &'a Self::Id,
events_since_last_snapshot: u64,
create_snapshot: Create,
) -> impl Future<Output = Result<SnapshotOffer, OfferSnapshotError<Self::Error, CE>>> + Send + 'a
where
CE: std::error::Error + Send + Sync + 'static,
Create: FnOnce() -> Result<Snapshot<Self::Position>, CE> + 'a;
}
The repository calls offer_snapshot after successfully appending new events. Implementations may decline without invoking create_snapshot, avoiding unnecessary snapshot encoding work.
The Snapshot Type
pub struct Snapshot<Pos> {
pub position: Pos,
pub data: Vec<u8>,
}
The data is the serialized aggregate state encoded using the repository’s event codec.
When to Snapshot
| Aggregate Type | Recommendation |
|---|---|
| Short-lived (< 100 events) | Skip snapshots |
| Medium (100-1000 events) | Every 100-500 events |
| Long-lived (1000+ events) | Every 100 events |
| High-throughput | Every 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 serialized form. When you change the struct:
- Add fields — Use
#[serde(default)]for backwards compatibility - Remove fields — Old snapshots still deserialize (extra fields ignored)
- Rename fields — Use
#[serde(alias = "old_name")] - 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