Snapshots
ES
As event-sourced aggregates accumulate events, loading them requires replaying every event from the beginning. Snapshots periodically store the aggregate's full state, so loading only needs to replay events since the last snapshot.
How snapshots work
- When loading an aggregate, Protean checks for a snapshot in the
snapshot stream (
{category}:snapshot-{id}) - If a snapshot exists, the aggregate is initialized from the snapshot and only post-snapshot events are replayed
- After loading, if the number of new events exceeds the snapshot threshold, a fresh snapshot is automatically created
Snapshots are an optimization. They are never the source of truth. The event stream is authoritative. You can delete all snapshots and rebuild them at any time.
Snapshots and schema changes
When you change an event-sourced aggregate's fields, a snapshot written before the change may no longer match the class. Renaming a field, removing one, or adding a required one leaves the stored snapshot out of step with the current schema, and it will not construct the aggregate any more.
Protean handles this on load. It reads the snapshot, and when the snapshot no longer constructs, it discards the snapshot and rebuilds the aggregate from the event stream, which is authoritative. Once the replayed event count reaches the threshold, a fresh snapshot in the current schema replaces the stale one, so later loads are fast again.
A stale snapshot is discarded and logged again on each load, without a fresh one
being written, in two cases: an at_version query whose requested version is at
or above the snapshot version, and an aggregate holding fewer events than the
threshold. Rebuild with protean snapshot create or
domain.create_snapshots(YourAggregate) to stop the repeat. An as_of query
never reads snapshots, so it neither uses nor discards them.
Each discard is logged as a WARNING on the
protean.snapshot channel, with
the aggregate name, its identifier, and the reason the snapshot was rejected.
Configuration
Set the snapshot threshold globally in domain.toml:
snapshot_threshold = 50
The default threshold is 10 events. When the number of events since the last snapshot meets or exceeds this value, a new snapshot is auto-created on the next load.
Manual snapshot creation
Programmatic API
with domain.domain_context():
# Snapshot a single aggregate instance
domain.create_snapshot(Account, "acc-001")
# Snapshot all instances of an aggregate
count = domain.create_snapshots(Account)
print(f"Created {count} snapshots")
# Snapshot all event-sourced aggregates
results = domain.create_all_snapshots()
for name, count in results.items():
print(f"{name}: {count} snapshots")
CLI
# Single aggregate instance
protean snapshot create --domain=myapp --aggregate=Account --identifier=acc-001
# All instances of one aggregate
protean snapshot create --domain=myapp --aggregate=Account
# All event-sourced aggregates in the domain
protean snapshot create --domain=myapp
Interaction with temporal queries
at_version=N: Uses the snapshot if the snapshot version is <= N, then applies remaining events up to version N.as_of=datetime: Ignores snapshots entirely and filters events by timestamp, since snapshots don't carry per-event timestamps.
No new snapshots are created during temporal queries.
When to snapshot
| Event volume | Strategy |
|---|---|
| < 100 events/aggregate | Default threshold (10) is fine |
| 100-10,000 events | Set threshold to 50-100 |
| > 10,000 events | Set threshold to 100-500; run periodic CLI snapshots |
For high-volume aggregates, schedule periodic snapshot creation as a cron job:
# Nightly snapshot refresh
0 2 * * * protean snapshot create --domain=myapp
See also
- Snapshot CLI Reference: Full CLI command documentation.
- Event Store Setup: Configuring the event store provider.
- Temporal Queries: Time-travel queries for event-sourced aggregates.