Snapshot
Snapshot is an important optimization mechanism in event sourcing architecture that improves performance by saving checkpoints of aggregate root state to reduce the number of event replays.
Snapshot Mechanism
In event sourcing, the state of an aggregate root is reconstructed by replaying all historical events. As the number of events increases, replaying all events becomes slower and slower. The snapshot mechanism solves this problem by periodically saving the current state of the aggregate root.
interface Snapshot<S : Any> : ReadOnlyStateAggregate<S>, SnapshotTimeCapable
data class SimpleSnapshot<S : Any>(
override val delegate: ReadOnlyStateAggregate<S>,
override val snapshotTime: Long = System.currentTimeMillis()
) : Snapshot<S>Snapshot Loading Flow
When loading an aggregate, the snapshot store is consulted first. If a snapshot exists, only events after the snapshot version need to be replayed.
Snapshot Strategies
Snapshot strategies determine when to create snapshots. The Wow framework provides multiple built-in strategies:
Version Offset Strategy (VersionOffset)
Creates a snapshot when the difference between the aggregate root version and the last snapshot version reaches a specified threshold.
class VersionOffsetSnapshotStrategy(
private val snapshotStore: SnapshotStore,
private val versionOffset: Int = 5
) : SnapshotStrategyAll Strategy (All)
Creates a snapshot for every state event.
class SimpleSnapshotStrategy(
private val snapshotStore: SnapshotStore
) : SnapshotStrategyNo Operation Strategy (NoOp)
Does not create any snapshots.
object NoOp : SnapshotStrategy {
override fun <S : Any> shouldSnapshot(stateEvent: StateEvent<S>): Boolean = false
}Snapshot Lifecycle
Snapshot Store
The snapshot store is responsible for storing and retrieving snapshots. Batch aggregate ID scanning belongs to EventStore.scanAggregateId(...), not to the snapshot store.
interface SnapshotStore : Named {
fun <S : Any> load(aggregateId: AggregateId): Mono<Snapshot<S>>
fun <S : Any> save(snapshot: Snapshot<S>): Mono<Void>
fun getVersion(aggregateId: AggregateId): Mono<Int>
}
interface VersionedSnapshotStore : SnapshotStore {
fun <S : Any> loadAtOrBefore(
aggregateId: AggregateId,
maxVersion: Int
): Mono<Snapshot<S>>
fun <S : Any> saveCheckpoint(snapshot: Snapshot<S>): Mono<Void>
}SnapshotStore.save() continues to maintain only the latest snapshot. VersionedSnapshotStore adds immutable historical checkpoints without changing that contract. It loads the greatest checkpoint no newer than a requested version. Duplicate aggregate/version writes are first-wins, and storage failures are propagated.
In-Memory Implementation
class InMemorySnapshotStore : SnapshotStore {
private val aggregateIdMapSnapshot = ConcurrentHashMap<AggregateId, String>()
override fun <S : Any> load(aggregateId: AggregateId): Mono<Snapshot<S>> =
Mono.fromCallable {
aggregateIdMapSnapshot[aggregateId]?.toObject<Snapshot<S>>()
}
override fun <S : Any> save(snapshot: Snapshot<S>): Mono<Void> =
Mono.fromRunnable {
aggregateIdMapSnapshot[snapshot.aggregateId] = snapshot.toJsonString()
}
}Supported Backends
| Backend | Module | Status |
|---|---|---|
| MongoDB | wow-mongo | Production-ready |
| Redis | wow-redis | Production-ready |
Snapshot Processing Flow
- State Event Publishing: When aggregate root state changes, publish state events
- Strategy Evaluation: Snapshot strategy evaluates whether a snapshot needs to be created
- Snapshot Creation: If needed, create a snapshot of the current state
- Snapshot Storage: Save the snapshot to the snapshot store
Configuration
wow:
eventsourcing:
snapshot:
enabled: true # Whether to enable snapshots
strategy: all # Snapshot strategy (all, version_offset)
version-offset: 5 # Version offset (only valid for version_offset strategy)
checkpoint:
enabled: false # Off by default; evaluate storage growth first
version-interval: 100| Property | Default | Description |
|---|---|---|
wow.eventsourcing.snapshot.enabled | true | Enable latest snapshots |
wow.eventsourcing.snapshot.checkpoint.enabled | false | Enable immutable historical checkpoints |
wow.eventsourcing.snapshot.checkpoint.version-interval | 100 | Checkpoint version interval |
Enabling the feature only accumulates newly produced matching versions; it does not backfill old history automatically. Roll back by disabling checkpoint.enabled. MongoDB stores checkpoints in the separate <aggregate>_snapshot_checkpoint sidecar.
Aggregate Loading Optimization
Snapshots greatly optimize aggregate root loading performance:
class EventSourcingOrderRepository(
private val eventStore: EventStore,
private val snapshotStore: SnapshotStore
) : OrderRepository {
override fun load(orderId: String): Mono<OrderState> {
val aggregateId = AggregateId("order", orderId)
return snapshotStore.load<OrderState>(aggregateId)
.flatMap { snapshot ->
// Only replay events after the snapshot version
eventStore.load(aggregateId, snapshot.version + 1)
.collectList()
.map { eventStreams ->
val state = snapshot.state
eventStreams.forEach { stream ->
stream.events.forEach { event ->
state.apply(event)
}
}
state
}
}
.switchIfEmpty(
// No snapshot, load all events
eventStore.load(aggregateId)
.collectList()
.map { eventStreams ->
val state = OrderState(orderId)
eventStreams.forEach { stream ->
stream.events.forEach { event ->
state.apply(event)
}
}
state
}
)
}
}Performance Impact
- Snapshots Enabled: Aggregate loading time is proportional to snapshot interval, not total event count
- Snapshots Disabled: Every load requires replaying all historical events
- Storage Cost: Requires additional storage space to save snapshot data
With a snapshot interval of 50, an aggregate with 1000 events replays at most 49 events instead of all 1000 -- a ~95% reduction.
Best Practices
- Choose Appropriate Snapshot Strategy: Select appropriate snapshot frequency based on business scenarios
- Monitor Snapshot Effectiveness: Regularly check if snapshots significantly improve loading performance
- Snapshot Cleanup: Regularly clean up expired snapshots to save storage space
- Snapshot Consistency: Ensure snapshot version consistency with event streams