Skip to main content

Snapshots

Snapshots checkpoint the actor's projected state at a known sequence number so that recovery can skip replaying the full event history from the beginning.

When to snapshot

Without snapshots, recovery replays every event the actor has ever written. For long-lived aggregates this grows unbounded. A snapshot sets a recovery baseline: on startup, the engine loads the latest snapshot and only replays events written after it.

The overhead of taking a snapshot is one store write per N events. The payoff is proportional to how many events you skip on recovery. For actors that write thousands of events before stopping, snapshots are essential.

SnapshotStrategy

SnapshotStrategy controls when the engine takes a snapshot. Configure it on EventSourcedBehavior:

src/Aggregates/OrderActor.php
<?php

declare(strict_types=1);

use Monadial\Nexus\Persistence\EventSourced\EventSourcedBehavior;
use Monadial\Nexus\Persistence\EventSourced\SnapshotStrategy;

$behavior = EventSourcedBehavior::create($persistenceId, $emptyState, $commandHandler, $eventHandler)
->withEventStore($eventStore)
->withSnapshotStore($snapshotStore)
->withSnapshotStrategy(SnapshotStrategy::everyN(50))
->toBehavior();

Three built-in strategies:

FactoryBehaviour
SnapshotStrategy::everyN(int $n)Snapshot after every N-th event (sequence number divisible by N)
SnapshotStrategy::never()Never snapshot — all recovery replays from sequence 0
SnapshotStrategy::predicate(Closure $fn)Custom predicate: fn(state, event, seqNr): bool
note

A snapshot store must be configured via ->withSnapshotStore($store) for snapshots to be taken. Setting a strategy without a store is silently ignored.

What a snapshot stores

The engine wraps the projected state in a SnapshotEnvelope:

FieldTypeDescription
persistenceIdPersistenceIdIdentity of the actor stream
sequenceNrintSequence number at the time of the snapshot
stateobjectThe serialized projected state
stateTypestringFQCN of the state class
timestampDateTimeImmutableWall-clock time of the snapshot
writerIdUlidIdentity of the ActorSystem that wrote it

The state object is serialized by the store's configured MessageSerializer (default: PhpNativeSerializer).

RetentionPolicy

After a snapshot is saved, old snapshots and — optionally — the events they cover can be pruned. Configure this via RetentionPolicy:

src/Aggregates/OrderActor.php
<?php

declare(strict_types=1);

use Monadial\Nexus\Persistence\EventSourced\EventSourcedBehavior;
use Monadial\Nexus\Persistence\EventSourced\RetentionPolicy;
use Monadial\Nexus\Persistence\EventSourced\SnapshotStrategy;

$behavior = EventSourcedBehavior::create($persistenceId, $emptyState, $commandHandler, $eventHandler)
->withEventStore($eventStore)
->withSnapshotStore($snapshotStore)
->withSnapshotStrategy(SnapshotStrategy::everyN(50))
->withRetention(RetentionPolicy::snapshotAndEvents(keepSnapshots: 3, deleteEventsTo: true))
->toBehavior();
FactoryBehaviour
RetentionPolicy::none()Keep all snapshots and all events forever
RetentionPolicy::snapshotAndEvents(int $keep, bool $deleteEventsTo)Keep the latest $keep snapshots; when $deleteEventsTo is true, also delete events up to the oldest retained snapshot's sequence number
Event deletion is permanent

Setting deleteEventsTo: true calls EventStore::deleteUpTo() after each snapshot. Events deleted this way cannot be recovered. Do not enable this if you need to project events into read models.

Recovery acceleration in practice

With SnapshotStrategy::everyN(50) and a RetentionPolicy that keeps 3 snapshots:

  • An actor with 10,000 events and a snapshot at event 9,950 recovers by loading one snapshot and replaying 50 events.
  • Without snapshots, the same actor replays all 10,000 events.

For actors that write many small events frequently (counters, metrics accumulators), use a smaller N (e.g. everyN(10)). For aggregates that process occasional domain commands, everyN(100) is a reasonable default.

See also