Skip to content

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:

  1. Builds an empty aggregate from its factory.
  2. Asks for the latest snapshot with matching SnapshotType and SnapshotVersion.
  3. If found, calls UnmarshalSnapshot and sets the aggregate version to the snapshot sequence.
  4. 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:

  1. Bump SnapshotVersion().
  2. Old snapshots stop matching and are ignored.
  3. 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.

Docs track the go-sequent library. When APIs drift, the source wins.