Skip to content

Snapshots ​

Every time you fetch an aggregate, the repository replays all its events from the beginning. For aggregates with many events, this gets slow. Snapshots capture the aggregate's state at a point in time, so only events after the snapshot need replaying.

How It Works ​

  1. On Save — after inserting events, the repository checks the snapshot schedule. If the schedule says yes, it serializes the aggregate and saves a snapshot.
  2. On Fetch — the repository loads the latest snapshot, sets the aggregate's version and state, then replays only events with a version higher than the snapshot.

This turns a replay of 10,000 events into loading one snapshot + replaying the last few events.

Snapshot Store ​

The snapshot store persists snapshots. All implementations satisfy the snapshot.Store interface:

go
import "github.com/modernice/goes/aggregate/snapshot"
MethodDescription
Save(ctx, snapshot)Save a snapshot
Latest(ctx, name, id)Get the most recent snapshot for an aggregate
Version(ctx, name, id, version)Get a snapshot at a specific version
Limit(ctx, name, id, version)Get the latest snapshot at or below a version
Query(ctx, query)Stream snapshots matching a query
Delete(ctx, snapshot)Delete a snapshot

Latest is the most common — the repository calls it on every Fetch to skip replaying events the snapshot already covers.

Implementations ​

Snapshot Schedule ​

The schedule determines when to take snapshots. The built-in option:

go
schedule := snapshot.Every(100)

This takes a snapshot every 100 events. More precisely, it returns true if any version in the range (lastSnapshotVersion, currentVersion] is divisible by 100.

For custom logic, implement the snapshot.Schedule interface:

go
type Schedule interface {
	Test(aggregate.Aggregate) bool
}

Configuring the Repository ​

Wire the snapshot store and schedule into the repository:

go
import (
	"github.com/modernice/goes/aggregate/repository"
	"github.com/modernice/goes/aggregate/snapshot"
	gomongo "github.com/modernice/goes/backend/mongo"
)

snapshots := gomongo.NewSnapshotStore(
	gomongo.SnapshotURL("mongodb://localhost:27017"),
)

repo := repository.New(store,
	repository.WithSnapshots(snapshots, snapshot.Every(100)),
)

That's all the wiring needed. The repository handles saving and loading snapshots automatically.

Encoding ​

For the repository to serialize your aggregate into a snapshot, the aggregate must implement one of these interfaces (checked in order):

  1. snapshot.Marshaler / snapshot.Unmarshaler
  2. encoding.BinaryMarshaler / encoding.BinaryUnmarshaler
  3. encoding.TextMarshaler / encoding.TextUnmarshaler

The simplest approach — marshal the DTO to JSON:

go
import "encoding/json"

func (p *Product) MarshalSnapshot() ([]byte, error) {
	return json.Marshal(p.ProductDTO)
}

func (p *Product) UnmarshalSnapshot(b []byte) error {
	return json.Unmarshal(b, &p.ProductDTO)
}

TIP

Only serialize the DTO, not the entire aggregate. *aggregate.Base handles its own version tracking — the snapshot system restores the version separately.

When to Use Snapshots ​

Snapshots add complexity. Use them when:

  • Aggregates commonly accumulate 100+ events
  • Fetch latency is noticeable in your application
  • You've measured that event replay is the bottleneck

For most applications, aggregates have a manageable number of events and snapshots aren't needed initially. Add them later when performance data justifies it.

Released under the Apache-2.0 License.