Reference
Snapshotting
Reference for go-sequent JSON snapshots: implementing SnapshotVersion, MarshalSnapshot and UnmarshalSnapshot, how the repository loads snapshots, the background Snapshotter, and versioning.
Snapshotting
Packages: snapshot, eventstore
A snapshot is a JSON copy of an aggregate’s state at a given sequence number. The repository loads the latest snapshot and then only the events after it. Snapshots speed up loading long-lived aggregates, such as an account with a very large stream. They are a cache. Events remain the source of truth.
Making an aggregate snapshotable
Implement three methods:
type Snapshotable interface {
SnapshotVersion() int
MarshalSnapshot() (json.RawMessage, error)
UnmarshalSnapshot(data json.RawMessage) error
}
From the bank Account:
const accountSnapshotVersion = 1
type accountSnap struct {
Owner string `json:"owner"`
Currency string `json:"currency"`
Balance types.Money `json:"balance"`
Open bool `json:"open"`
}
func (a *Account) SnapshotVersion() int { return accountSnapshotVersion }
func (a *Account) MarshalSnapshot() (json.RawMessage, error) {
return json.Marshal(accountSnap{
Owner: a.Owner, Currency: a.Currency, Balance: a.Balance, Open: a.Open,
})
}
func (a *Account) UnmarshalSnapshot(data json.RawMessage) error {
var s accountSnap
if err := json.Unmarshal(data, &s); err != nil {
return err
}
a.Owner, a.Currency, a.Balance, a.Open = s.Owner, s.Currency, s.Balance, s.Open
if a.Open {
a.refreshUniqueKeys()
}
return nil
}
Use a dedicated snapshot struct, not the aggregate itself. It makes the stored shape explicit and lets it change on its own schedule. Restore anything the applier would normally derive, such as unique keys, because UnmarshalSnapshot replaces the replay of earlier events.
The snapshot format is JSON only.
What the store keeps
type Snapshot struct {
AggregateID uuid.UUID
SequenceNumber int64
SnapshotType string // usually the aggregate type name
SnapshotVersion int
JSON json.RawMessage
CreatedAt time.Time
}
eventstore.SnapshotStore has SaveSnapshot, LoadLatestSnapshot, MarkSnapshotTaken and ListNeedingSnapshots. The memory and Postgres stores implement it. Postgres uses the tables snapshot_records and aggregates_that_need_snapshots.
Loading
Set Repo.Snapshots to a snapshot store. On Load, the repository:
- Builds an empty aggregate from its factory.
- Asks for the latest snapshot with matching
SnapshotTypeandSnapshotVersion. - If found, calls
UnmarshalSnapshotand sets the aggregate version to the snapshot sequence. - Loads the stream and replays only events after that sequence.
A snapshot with a different version is ignored, so the aggregate falls back to loading all events.
Taking snapshots
When an append moves a stream past its threshold, the store marks the aggregate as needing a snapshot. In Postgres the threshold is snapshot_threshold on the aggregate row, defaulting to 100. For the memory store, call SetSnapshotThreshold(n).
snapshot.Snapshotter takes the snapshots in the background:
sn := &snapshot.Snapshotter{
Store: store,
Snapshots: store,
Factories: map[string]aggregate.Factory{"bank.Account": func(id uuid.UUID) aggregate.Root { return NewAccount(id) }},
ResolveType: func(ctx context.Context, id uuid.UUID) (string, error) { /* aggregate type for id */ },
Interval: time.Minute,
BatchSize: 10,
}
n, err := sn.RunOnce(ctx) // or sn.Run(ctx)
For each aggregate that needs one, it loads the stream, rebuilds the aggregate, calls snapshot.Take, saves the result and marks it taken. Take returns an error if the aggregate is not Snapshotable.
Changing the aggregate shape
When the stored snapshot shape or the meaning of state changes in an incompatible way:
- Bump
SnapshotVersion(). - Old snapshots stop matching and are ignored.
- The snapshotter refills them over time.
You can also delete the old snapshot_records rows. Because snapshots can always be regenerated from events, a suspect or poisoned snapshot is never fatal. After restoring from backup, drop incompatible snapshots too. See Backup and restore and Event evolution.
When to use them
Add snapshots after you measure slow loads on long streams. Short streams load fast, and a snapshot adds a second code path to keep correct.
Related
Docs track the go-sequent library. When APIs drift, the source wins.