Snapshot Setup
Snapshots store the current state of an aggregate, avoiding the need to replay all historical events. This is essential for aggregates with many events.
Before You Start
- .NET 8.0+ (or .NET 9/10 for latest features)
- Install the required packages:
dotnet add package Excalibur.EventSourcing
dotnet add package Excalibur.EventSourcing.SqlServer # or your provider - Familiarity with event stores and event store setup
Why Use Snapshots?
Without snapshots:
Load aggregate with 10,000 events
→ Replay all 10,000 events
→ Slow load time, high memory usage
With snapshots (every 100 events):
Load snapshot at version 9,900
→ Replay only 100 events
→ Fast load time, consistent performance
Basic Setup
// Configure event sourcing with snapshot strategy
services.AddExcaliburEventSourcing(builder =>
{
builder.AddRepository<OrderAggregate, Guid>();
builder.UseIntervalSnapshots(100); // Snapshot every 100 events
});
// Add the SQL Server event store and snapshot store
services.AddSqlServerEventSourcing(connectionString);
Snapshot Strategies
Interval-Based (Recommended)
Create a snapshot every N events:
builder.UseIntervalSnapshots(100);
| Event Count | Recommended Interval |
|---|---|
| < 1,000 | 50-100 |
| 1,000 - 10,000 | 100-500 |
| > 10,000 | 500-1,000 |
Time-Based
Create snapshots after a time interval:
builder.UseTimeBasedSnapshots(TimeSpan.FromHours(1));
Useful when:
- Event frequency varies significantly
- You want predictable snapshot timing
- Aggregates receive bursts of events
Size-Based
Create snapshots when event data exceeds a size threshold:
builder.UseSizeBasedSnapshots(maxSizeInBytes: 1_000_000); // 1 MB
Useful when:
- Events vary significantly in size
- Memory usage is a primary concern
Composite Strategy
Combine multiple strategies with OR logic:
builder.UseCompositeSnapshotStrategy(composite =>
{
composite.AddIntervalStrategy(100) // Every 100 events
.AddTimeBasedStrategy(TimeSpan.FromHours(1)) // OR every hour
.AddSizeBasedStrategy(500_000); // OR when > 500 KB
});
No Snapshots
For aggregates with few events:
builder.UseNoSnapshots();
Snapshot Storage
Inline with Events
Store snapshots in the same database as events (default):
// SQL Server stores snapshots in the Snapshots table alongside events
services.AddSqlServerEventSourcing(connectionString);
Separate Store
Use a different storage backend for snapshots:
// Register event store and a custom snapshot store
services.AddSqlServerEventStore(connectionString);
// Use a custom snapshot manager
services.AddExcaliburEventSourcing(builder =>
{
builder.UseSnapshotManager<RedisSnapshotManager>();
});
Implementing Snapshot Methods
Your aggregate must override the snapshot methods from AggregateRoot. The ISnapshot interface uses byte[] Data for serialized state — you define a domain state type and serialize it:
public class Order : AggregateRoot<OrderId>
{
public string CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public decimal TotalAmount { get; private set; }
private readonly List<OrderItem> _items = new();
// Override to create snapshot from current state
public override ISnapshot CreateSnapshot()
{
// Create domain state object (NOT ISnapshot)
var state = new OrderSnapshotState
{
CustomerId = CustomerId,
Status = Status,
TotalAmount = TotalAmount,
Items = _items.Select(i => new OrderItemState
{
Sku = i.Sku,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}).ToList()
};
// Serialize to bytes and wrap in Snapshot using factory method
return Snapshot.Create(
aggregateId: Id.ToString(),
version: Version,
data: JsonSerializer.SerializeToUtf8Bytes(state),
aggregateType: nameof(Order));
}
// Override to restore state from snapshot
protected override void ApplySnapshot(ISnapshot snapshot)
{
// Deserialize state from snapshot.Data
var state = JsonSerializer.Deserialize<OrderSnapshotState>(snapshot.Data)
?? throw new InvalidOperationException("Failed to deserialize snapshot");
CustomerId = state.CustomerId;
Status = state.Status;
TotalAmount = state.TotalAmount;
_items.Clear();
_items.AddRange(state.Items.Select(i => new OrderItem
{
Sku = i.Sku,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}));
}
}
// Domain state to be serialized — NOT ISnapshot
public record OrderSnapshotState
{
public string CustomerId { get; init; } = string.Empty;
public OrderStatus Status { get; init; }
public decimal TotalAmount { get; init; }
public List<OrderItemState> Items { get; init; } = new();
}
public record OrderItemState
{
public string Sku { get; init; } = string.Empty;
public int Quantity { get; init; }
public decimal UnitPrice { get; init; }
}
Custom Snapshot Strategy
Implement ISnapshotStrategy for custom logic:
public class BusinessHoursSnapshotStrategy : ISnapshotStrategy
{
public bool ShouldCreateSnapshot(IAggregateRoot aggregate)
{
// Only snapshot during off-peak hours
var hour = DateTime.UtcNow.Hour;
var isOffPeak = hour < 6 || hour > 22;
return isOffPeak && aggregate.Version % 50 == 0;
}
}
// Register
builder.AddSnapshotStrategy<BusinessHoursSnapshotStrategy>();
Snapshot Manager
Control when snapshots are created:
public interface ISnapshotManager
{
Task SaveSnapshotAsync<TAggregate>(TAggregate aggregate, CancellationToken ct)
where TAggregate : IAggregateRoot;
Task<TSnapshot?> LoadSnapshotAsync<TSnapshot>(string streamId, CancellationToken ct);
}
Manual Snapshots
Force a snapshot outside normal strategy:
var manager = services.GetRequiredService<ISnapshotManager>();
await manager.SaveSnapshotAsync(aggregate, ct);
Bulk Snapshot Creation
For existing aggregates without snapshots:
public class SnapshotMigrationJob
{
public async Task MigrateAsync(CancellationToken ct)
{
var aggregateIds = await _eventStore.GetAllStreamIdsAsync(ct);
foreach (var id in aggregateIds)
{
var aggregate = await _repository.GetByIdAsync(id, ct);
if (aggregate is not null)
{
var snapshot = aggregate.CreateSnapshot();
await _snapshotStore.SaveSnapshotAsync(snapshot, ct);
}
}
}
}
Performance Considerations
Snapshot Serialization
Choose efficient serialization:
builder.UseEventSerializer<MessagePackSnapshotSerializer>();
| Serializer | Size | Speed | Human-Readable |
|---|---|---|---|
| JSON | Large | Medium | Yes |
| MessagePack | Small | Fast | No |
| MemoryPack | Smallest | Fastest | No |
Snapshot Size
Keep snapshots small:
// DON'T: Include derived data
public record BadSnapshot
{
public List<OrderItem> Items { get; init; }
public decimal TotalAmount { get; init; } // Can be computed from Items
}
// DO: Only include essential state
public record GoodSnapshot
{
public List<OrderItem> Items { get; init; }
// TotalAmount computed when needed
}
Monitoring
Snapshot Metrics
services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddEventSourcingInstrumentation();
});
Metrics:
excalibur.snapshots.created— Snapshots createdexcalibur.snapshots.loaded— Snapshots loadedexcalibur.snapshots.size_bytes— Snapshot size distributionexcalibur.events.replayed— Events replayed after snapshot
Health Check
Snapshot store health is monitored automatically when RegisterHealthChecks = true in SqlServerEventSourcingOptions (default). See Event Store Setup for details.
Best Practices
| Practice | Reason |
|---|---|
| Choose appropriate interval | Balance storage vs. replay time |
| Override snapshot methods | Required for snapshot support |
| Keep snapshots small | Faster serialization/deserialization |
| Monitor replay counts | Detect missing snapshot coverage |
| Test snapshot restoration | Ensure state is correctly restored |
Troubleshooting
Snapshot not being created
- Verify aggregate overrides
CreateSnapshot()andApplySnapshot() - Check strategy threshold is being reached
- Verify snapshot store is configured
State mismatch after restore
- Verify
CreateSnapshot()captures all state - Verify
ApplySnapshot()restores all state - Check for missing private fields
Large snapshot size
- Review what's included in snapshot
- Remove derived/computed data
- Consider more compact serialization
See Also
- Snapshots — Snapshot concepts and how they integrate with event sourcing
- Event Store Setup — Configure event stores and aggregate repositories
- Aggregates — Aggregate root design, including snapshot method overrides
- Event Sourcing Overview — Introduction to event sourcing patterns in Excalibur