Snapshots in Event Sourcing
Why replaying an aggregate's entire event history on every single command becomes a real performance problem for long-lived aggregates, and how a periodic snapshot lets Axon skip straight to a recent state and replay only what happened since.
Learning objectives
- Explain why replaying every event on every command load becomes a genuine performance problem
- Configure a snapshot trigger for an aggregate using EventCountSnapshotTriggerDefinition
- Explain how aggregate loading changes once a snapshot exists
- Judge which aggregates actually need snapshotting and which don't
◆ Story
If someone asks what a person looks like today, you don't hand them a complete written account of every day of that person's life and ask them to read it cover to cover. A single recent photograph, plus a quick note about anything notable that's happened since it was taken, answers the question instantly. The full life story is still real, still complete, and still exists somewhere — you just don't need to re-read all of it to answer this one question.
A snapshot is exactly that photograph, applied to an aggregate: a saved copy of its state at a specific point in its event history. With a snapshot available, Axon can load that saved state directly instead of replaying every event an aggregate has ever applied, from the very first one, on every single load.
◆ The problem
Loading an aggregate normally means replaying its entire event history, in order, from the very first event it ever applied, every single time a new command needs to run against it. For a customer aggregate with five events total, that's instant — nobody notices the cost. For a heavily used bank account aggregate with fifty thousand transaction events accumulated over several years, replaying every single one of those fifty thousand events, every single time a new command arrives, becomes a real, measurable performance problem. A deposit command that should take milliseconds ends up spending most of its time just reconstructing state that hasn't meaningfully changed in years.
This cost scales with how long-lived and how active an aggregate is, not with how complex any individual event is. A customer aggregate that rarely changes will likely never need a snapshot. An account aggregate at the center of years of daily transactions absolutely will, eventually, if left unaddressed. The fix isn't to store less history — the permanent event history has real value and should never be discarded — it's to avoid re-deriving state from that entire history on every load.
Enabling snapshotting for an aggregate is entirely configuration — nothing about the aggregate's own event-sourcing handlers needs to change. You tell Axon how often to take a snapshot, and it handles the rest automatically.
@Aggregate(snapshotTriggerDefinition = "accountSnapshotTrigger") points AccountAggregate at a named SnapshotTriggerDefinition bean. EventCountSnapshotTriggerDefinition is the simplest trigger strategy: take a new snapshot every N events applied by this aggregate — 50, in this example. Axon takes care of serializing the aggregate's current state into a snapshot and storing it once that threshold is crossed; none of that logic needs to be written by hand.
Other trigger strategies exist for different needs — for example, triggering based on elapsed time rather than event count — but event-count triggering is the most common starting point, since it directly targets the actual cost driver: how many events would otherwise need replaying.
💻 Code example
@Aggregate(snapshotTriggerDefinition = "accountSnapshotTrigger") public class AccountAggregate { // Nothing about the aggregate's own code needs to change at all. } @Configuration public class SnapshotConfig { @Bean public SnapshotTriggerDefinition accountSnapshotTrigger(Snapshotter snapshotter) { return new EventCountSnapshotTriggerDefinition(snapshotter, 50); // Snapshot every 50 events. } }
Once a snapshot exists, loading an aggregate changes shape. Instead of "replay every event from the first one," it becomes "load the most recent snapshot, then replay only the events that happened after it."
Picture an account with 53 total events, snapshotted at event #50. Loading this aggregate now means: load the snapshot taken at event #50 directly (skipping the replay of events #1 through #50 entirely), then replay only events #51, #52, and #53 on top of it. Three events get replayed instead of fifty-three — and that gap only widens as more events accumulate between snapshots.
◆ Under the hood
Enabling snapshots doesn't delete or replace any of the original events — the complete, permanent history remains fully intact in the event store, exactly as before. A snapshot is purely an additional, disposable optimization sitting alongside that history, conceptually similar to how a projection's read model is disposable and rebuildable. You could delete every snapshot in the system at any time, and it would keep working correctly — just slower, back to replaying full history on every load, exactly as it behaved before snapshotting was ever configured.
This is worth internalizing because it removes any anxiety about snapshotting being risky: a snapshot is never the only copy of anything. It's a cache of a calculation, not a second source of truth. If a snapshot ever gets corrupted or goes stale in a way that matters, deleting it and letting Axon rebuild it from full history is always a safe, correct fallback.
This kind of optimization matters most for aggregates that stay in heavy, long-term use — a long-lived, actively transacted bank account being the clearest real-world example — where the gap between "replay everything" and "replay since the last snapshot" only keeps growing over the aggregate's lifetime.
▲ Common mistake
Setting a snapshot trigger threshold that's far too low — snapshotting every 2 or 5 events, for example — creates unnecessary snapshot storage and write overhead for aggregates that were never actually slow to replay in the first place. Every snapshot taken is itself a write, and a serialized copy of aggregate state that needs storing and eventually cleaning up. An aggregate that only ever accumulates a few dozen events over its lifetime gains nothing from snapshotting and just pays that overhead for no benefit.
Reserve snapshotting for aggregates that genuinely accumulate a large number of events over a long lifetime — a long-lived bank account being the canonical example — rather than enabling it universally, by default, for every aggregate type in a system. A reasonable approach is to leave snapshotting off until profiling or production load actually shows aggregate loading time becoming a measurable bottleneck for a specific aggregate type, then configure a trigger threshold sized to that aggregate's real event volume.
A second pitfall: forgetting that a change to an aggregate's internal fields (adding, renaming, or removing state) can make old snapshots incompatible with the current aggregate code. If a snapshot fails to deserialize correctly after a change like this, the safe response is to delete the stale snapshots and let Axon rebuild fresh ones from full event history — never to try to hand-patch a serialized snapshot to match new code.
Q: What specific performance problem do snapshots solve? A: Replaying an aggregate's entire event history on every single command load, which becomes measurably slow for long-lived, heavily used aggregates with tens of thousands of events.
Q: Does enabling snapshots delete or replace any of the original events? A: No. The complete event history remains fully intact in the event store — a snapshot is a purely additional, disposable optimization sitting alongside it.
Q: With a snapshot in place, what does Axon actually replay when loading an aggregate? A: Only the events that occurred after the snapshot was taken, not the aggregate's entire history from the beginning.
Q: What kind of trigger does EventCountSnapshotTriggerDefinition use? A: It takes a new snapshot automatically after a configured number of events have been applied by the aggregate.
Q: Which aggregates actually need snapshotting? A: Long-lived aggregates that accumulate a large number of events over time, like an active bank account — not every aggregate type by default, since snapshotting a rarely-changing aggregate adds overhead with no real benefit.
Want a visual for this concept?
Generate a diagram tailored to “Snapshots in Event Sourcing” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.
Sign in to generate a visual →