Exercise 05 · Shortening the load of a long stream

Snapshot

  • snapshot
  • the cost of replay
  • aggregate code version

Principle

Loading an aggregate replays all events. With a long stream this takes time (the panel shows “replayed N events”).

A snapshot is the stored state of an aggregate at a specific stream version. On load, the snapshot is taken and only newer events are replayed.

A snapshot is a cache, not the source of truth. When the aggregate code changes, the old snapshot must be discarded and the stream replayed again.

What to watch

  • Number of events replayed on load.
  • After “Deploy a new aggregate version”, the snapshot is ignored and everything is replayed.

Without the solution / With the solution

Without the solution

Run 300 changes without a snapshot

The adviser made 300 small changes. Every aggregate load replays all 301 events.

Run in the simulator without the solution

With the solution

Run with a snapshot every 50 events

The same 300 changes, but loading replays only the events since the last snapshot. Try “Deploy a new aggregate version”: the snapshot is invalidated.

Run in the simulator with the solution

What to do: Click “Run 300 changes without a snapshot”. What to look at: Loading the aggregate replayed 301 events (Aggregate panel). The simulator needs JavaScript and runs as a separate application.

Try it in your own project

Copy the prompt into your coding agent (Claude Code, Copilot, Codex…) and run it in your project's repository.

Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

Language
Variant
# Task for a coding agent: Snapshot

Stack: Java. Mode: guided.
Goal: Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

## Context of the existing code

You are working in an existing project (Java backend (typically Java 21, Spring Boot, JPA or JDBC). Use the build tool and style the project already has (Maven/Gradle, JUnit 5).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store for calculations (Event Sourcing) and an aggregate loaded by replaying the stream. If not, first create a minimal one: append-only events and `load(streamId)` as a fold over a pure `apply`.

Read first, change nothing. Find and briefly summarise:
- The aggregate loading code (fold), who calls it and what they use from the result, and the typical and longest stream lengths in the data.
- Aggregate code versioning (how do you tell that apply has changed?). If it is missing, propose a version constant and a test over a canonical history that fails when the result of `apply` changes.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (snapshot storage), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

A snapshot is the stored state of an aggregate at a specific stream version: loading takes the snapshot and replays only newer events. A snapshot is a cache, not the source of truth; after the aggregate code changes it is ignored and the whole stream is replayed. Example: after 300 changes, loading without a snapshot replays 301 events, with a snapshot every 10 events at most 9.

## Specific task

Technical specifics (Java): Snapshot as a JSON column (Jackson), keep the code version in an aggregate constant.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: a snapshot is created after a write when at least N events have been added since the last one (N in configuration), and where it is stored. Use it for commands; leave reads that need the full history unchanged.
2. Store the snapshot (streamId, version, stateJson, aggregateCodeVersion).
3. Loading: use the snapshot only when aggregateCodeVersion matches, otherwise do a full replay.
4. Property test: for random command sequences and a small N (1–8), the state and version from the snapshot equal a full replay; verify that snapshots were used (a seeded generator is enough).
5. Measure the number of replayed events before and after, for a typical calculation and for a long stream (e.g. 100 events).

Watch out for:
- A snapshot never replaces the stream: after a snapshot nothing is deleted from or truncated in the stream.
- State serialisation must survive adding a field to the aggregate (default value); if the result of `apply` changes for old events, bump the code version.

## What you must not change

- The existing public API and the behaviour of the calculator endpoints that currently work.
- The database schema of existing tables (new tables/migrations are fine; renaming and deleting are not).
- Other people's tests, seed data and existing values. If a change is necessary, put it in a separate commit and state it. If a change to an event's shape or a new consumer necessarily changes an existing test, update it in a separate commit and explain why in the summary.
- Public contracts (APIs, event schemas) without backward compatibility.

## Done when

- Test of snapshot vs. full replay equality.
- An invalid snapshot (different code version) is ignored; changing the result of `apply` without bumping the version fails the test.
- The saving is backed by measuring the number of replayed events.
- JUnit 5 tests in the existing `src/test/java` structure pass with the project's build tool.
- In the summary, state: what you changed, what you assumed, what remains.
# Task for a coding agent: Snapshot

Stack: Java. Mode: challenge.
Goal: Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

## Context of the existing code

You are working in an existing project (Java backend (typically Java 21, Spring Boot, JPA or JDBC). Use the build tool and style the project already has (Maven/Gradle, JUnit 5).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store for calculations (Event Sourcing) and an aggregate loaded by replaying the stream. If not, first create a minimal one: append-only events and `load(streamId)` as a fold over a pure `apply`.

Read first, change nothing. Find and briefly summarise:
- The aggregate loading code (fold), who calls it and what they use from the result, and the typical and longest stream lengths in the data.
- Aggregate code versioning (how do you tell that apply has changed?). If it is missing, propose a version constant and a test over a canonical history that fails when the result of `apply` changes.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (snapshot storage), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

A snapshot is the stored state of an aggregate at a specific stream version: loading takes the snapshot and replays only newer events. A snapshot is a cache, not the source of truth; after the aggregate code changes it is ignored and the whole stream is replayed. Example: after 300 changes, loading without a snapshot replays 301 events, with a snapshot every 10 events at most 9.

## Brief (Challenge)

Introduce snapshots for the calculator aggregate so that the result always equals a full replay, and measure the saving.

Technical specifics (Java): Snapshot as a JSON column (Jackson), keep the code version in an aggregate constant.

Choose your own approach. The pitfalls in this topic are not listed: find them (concurrency, redelivery, ordering, failure midway) and for each write a test that would expose it. At the end, list which pitfalls you found and which you deliberately left open.

## What you must not change

- The existing public API and the behaviour of the calculator endpoints that currently work.
- The database schema of existing tables (new tables/migrations are fine; renaming and deleting are not).
- Other people's tests, seed data and existing values. If a change is necessary, put it in a separate commit and state it. If a change to an event's shape or a new consumer necessarily changes an existing test, update it in a separate commit and explain why in the summary.
- Public contracts (APIs, event schemas) without backward compatibility.

## Done when

- Test of snapshot vs. full replay equality.
- An invalid snapshot (different code version) is ignored; changing the result of `apply` without bumping the version fails the test.
- The saving is backed by measuring the number of replayed events.
- JUnit 5 tests in the existing `src/test/java` structure pass with the project's build tool.
- In the summary, state: what you changed, what you assumed, what remains.
# Task for a coding agent: Snapshot

Stack: C#. Mode: guided.
Goal: Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

## Context of the existing code

You are working in an existing project (C# backend (.NET, ASP.NET Core, EF Core or Dapper). Use the solution, style and DI conventions the project already has (xUnit/NUnit).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store for calculations (Event Sourcing) and an aggregate loaded by replaying the stream. If not, first create a minimal one: append-only events and `load(streamId)` as a fold over a pure `apply`.

Read first, change nothing. Find and briefly summarise:
- The aggregate loading code (fold), who calls it and what they use from the result, and the typical and longest stream lengths in the data.
- Aggregate code versioning (how do you tell that apply has changed?). If it is missing, propose a version constant and a test over a canonical history that fails when the result of `apply` changes.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (snapshot storage), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

A snapshot is the stored state of an aggregate at a specific stream version: loading takes the snapshot and replays only newer events. A snapshot is a cache, not the source of truth; after the aggregate code changes it is ignored and the whole stream is replayed. Example: after 300 changes, loading without a snapshot replays 301 events, with a snapshot every 10 events at most 9.

## Specific task

Technical specifics (C#): Snapshot via `System.Text.Json`, code version as a constant of the aggregate type.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: a snapshot is created after a write when at least N events have been added since the last one (N in configuration), and where it is stored. Use it for commands; leave reads that need the full history unchanged.
2. Store the snapshot (streamId, version, stateJson, aggregateCodeVersion).
3. Loading: use the snapshot only when aggregateCodeVersion matches, otherwise do a full replay.
4. Property test: for random command sequences and a small N (1–8), the state and version from the snapshot equal a full replay; verify that snapshots were used (a seeded generator is enough).
5. Measure the number of replayed events before and after, for a typical calculation and for a long stream (e.g. 100 events).

Watch out for:
- A snapshot never replaces the stream: after a snapshot nothing is deleted from or truncated in the stream.
- State serialisation must survive adding a field to the aggregate (default value); if the result of `apply` changes for old events, bump the code version.

## What you must not change

- The existing public API and the behaviour of the calculator endpoints that currently work.
- The database schema of existing tables (new tables/migrations are fine; renaming and deleting are not).
- Other people's tests, seed data and existing values. If a change is necessary, put it in a separate commit and state it. If a change to an event's shape or a new consumer necessarily changes an existing test, update it in a separate commit and explain why in the summary.
- Public contracts (APIs, event schemas) without backward compatibility.

## Done when

- Test of snapshot vs. full replay equality.
- An invalid snapshot (different code version) is ignored; changing the result of `apply` without bumping the version fails the test.
- The saving is backed by measuring the number of replayed events.
- Tests in the existing test project pass (`dotnet test`).
- In the summary, state: what you changed, what you assumed, what remains.
# Task for a coding agent: Snapshot

Stack: C#. Mode: challenge.
Goal: Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

## Context of the existing code

You are working in an existing project (C# backend (.NET, ASP.NET Core, EF Core or Dapper). Use the solution, style and DI conventions the project already has (xUnit/NUnit).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store for calculations (Event Sourcing) and an aggregate loaded by replaying the stream. If not, first create a minimal one: append-only events and `load(streamId)` as a fold over a pure `apply`.

Read first, change nothing. Find and briefly summarise:
- The aggregate loading code (fold), who calls it and what they use from the result, and the typical and longest stream lengths in the data.
- Aggregate code versioning (how do you tell that apply has changed?). If it is missing, propose a version constant and a test over a canonical history that fails when the result of `apply` changes.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (snapshot storage), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

A snapshot is the stored state of an aggregate at a specific stream version: loading takes the snapshot and replays only newer events. A snapshot is a cache, not the source of truth; after the aggregate code changes it is ignored and the whole stream is replayed. Example: after 300 changes, loading without a snapshot replays 301 events, with a snapshot every 10 events at most 9.

## Brief (Challenge)

Introduce snapshots for the calculator aggregate so that the result always equals a full replay, and measure the saving.

Technical specifics (C#): Snapshot via `System.Text.Json`, code version as a constant of the aggregate type.

Choose your own approach. The pitfalls in this topic are not listed: find them (concurrency, redelivery, ordering, failure midway) and for each write a test that would expose it. At the end, list which pitfalls you found and which you deliberately left open.

## What you must not change

- The existing public API and the behaviour of the calculator endpoints that currently work.
- The database schema of existing tables (new tables/migrations are fine; renaming and deleting are not).
- Other people's tests, seed data and existing values. If a change is necessary, put it in a separate commit and state it. If a change to an event's shape or a new consumer necessarily changes an existing test, update it in a separate commit and explain why in the summary.
- Public contracts (APIs, event schemas) without backward compatibility.

## Done when

- Test of snapshot vs. full replay equality.
- An invalid snapshot (different code version) is ignored; changing the result of `apply` without bumping the version fails the test.
- The saving is backed by measuring the number of replayed events.
- Tests in the existing test project pass (`dotnet test`).
- In the summary, state: what you changed, what you assumed, what remains.
# Task for a coding agent: Snapshot

Stack: Web (HTML/JS or the app's stack). Mode: guided.
Goal: Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

## Context of the existing code

You are working in an existing project (web application: a browser frontend, possibly with its own backend. If the project has a backend, the event store, broker and services belong on the backend, and the points about endpoints and tables refer to it. If it has no backend, use IndexedDB in the browser and `fake-indexeddb` in tests. Keep the project's framework and test runner; add new dependencies only with a justification.).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store for calculations (Event Sourcing) and an aggregate loaded by replaying the stream. If not, first create a minimal one: append-only events and `load(streamId)` as a fold over a pure `apply`.

Read first, change nothing. Find and briefly summarise:
- The aggregate loading code (fold), who calls it and what they use from the result, and the typical and longest stream lengths in the data.
- Aggregate code versioning (how do you tell that apply has changed?). If it is missing, propose a version constant and a test over a canonical history that fails when the result of `apply` changes.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (snapshot storage), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

A snapshot is the stored state of an aggregate at a specific stream version: loading takes the snapshot and replays only newer events. A snapshot is a cache, not the source of truth; after the aggregate code changes it is ignored and the whole stream is replayed. Example: after 300 changes, loading without a snapshot replays 301 events, with a snapshot every 10 events at most 9.

## Specific task

Technical specifics (Web (HTML/JS or the app's stack)): Snapshot next to the event store (a table on the backend, without a backend an IndexedDB object store `snapshots`), keyed by streamId. The event store must be able to read a stream from a version; if it cannot, add it.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: a snapshot is created after a write when at least N events have been added since the last one (N in configuration), and where it is stored. Use it for commands; leave reads that need the full history unchanged.
2. Store the snapshot (streamId, version, stateJson, aggregateCodeVersion).
3. Loading: use the snapshot only when aggregateCodeVersion matches, otherwise do a full replay.
4. Property test: for random command sequences and a small N (1–8), the state and version from the snapshot equal a full replay; verify that snapshots were used (a seeded generator is enough).
5. Measure the number of replayed events before and after, for a typical calculation and for a long stream (e.g. 100 events).

Watch out for:
- A snapshot never replaces the stream: after a snapshot nothing is deleted from or truncated in the stream.
- State serialisation must survive adding a field to the aggregate (default value); if the result of `apply` changes for old events, bump the code version.

## What you must not change

- The existing public API and the behaviour of the calculator endpoints that currently work.
- The database schema of existing tables (new tables/migrations are fine; renaming and deleting are not).
- Other people's tests, seed data and existing values. If a change is necessary, put it in a separate commit and state it. If a change to an event's shape or a new consumer necessarily changes an existing test, update it in a separate commit and explain why in the summary.
- Public contracts (APIs, event schemas) without backward compatibility.

## Done when

- Test of snapshot vs. full replay equality.
- An invalid snapshot (different code version) is ignored; changing the result of `apply` without bumping the version fails the test.
- The saving is backed by measuring the number of replayed events.
- Tests pass with the existing test runner; lint and build pass if the project has them.
- In the summary, state: what you changed, what you assumed, what remains.
# Task for a coding agent: Snapshot

Stack: Web (HTML/JS or the app's stack). Mode: challenge.
Goal: Add aggregate snapshots and verify that the state from a snapshot + newer events equals a full replay.

## Context of the existing code

You are working in an existing project (web application: a browser frontend, possibly with its own backend. If the project has a backend, the event store, broker and services belong on the backend, and the points about endpoints and tables refer to it. If it has no backend, use IndexedDB in the browser and `fake-indexeddb` in tests. Keep the project's framework and test runner; add new dependencies only with a justification.).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store for calculations (Event Sourcing) and an aggregate loaded by replaying the stream. If not, first create a minimal one: append-only events and `load(streamId)` as a fold over a pure `apply`.

Read first, change nothing. Find and briefly summarise:
- The aggregate loading code (fold), who calls it and what they use from the result, and the typical and longest stream lengths in the data.
- Aggregate code versioning (how do you tell that apply has changed?). If it is missing, propose a version constant and a test over a canonical history that fails when the result of `apply` changes.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (snapshot storage), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

A snapshot is the stored state of an aggregate at a specific stream version: loading takes the snapshot and replays only newer events. A snapshot is a cache, not the source of truth; after the aggregate code changes it is ignored and the whole stream is replayed. Example: after 300 changes, loading without a snapshot replays 301 events, with a snapshot every 10 events at most 9.

## Brief (Challenge)

Introduce snapshots for the calculator aggregate so that the result always equals a full replay, and measure the saving.

Technical specifics (Web (HTML/JS or the app's stack)): Snapshot next to the event store (a table on the backend, without a backend an IndexedDB object store `snapshots`), keyed by streamId. The event store must be able to read a stream from a version; if it cannot, add it.

Choose your own approach. The pitfalls in this topic are not listed: find them (concurrency, redelivery, ordering, failure midway) and for each write a test that would expose it. At the end, list which pitfalls you found and which you deliberately left open.

## What you must not change

- The existing public API and the behaviour of the calculator endpoints that currently work.
- The database schema of existing tables (new tables/migrations are fine; renaming and deleting are not).
- Other people's tests, seed data and existing values. If a change is necessary, put it in a separate commit and state it. If a change to an event's shape or a new consumer necessarily changes an existing test, update it in a separate commit and explain why in the summary.
- Public contracts (APIs, event schemas) without backward compatibility.

## Done when

- Test of snapshot vs. full replay equality.
- An invalid snapshot (different code version) is ignored; changing the result of `apply` without bumping the version fails the test.
- The saving is backed by measuring the number of replayed events.
- Tests pass with the existing test runner; lint and build pass if the project has them.
- In the summary, state: what you changed, what you assumed, what remains.

Training for your team

Would you like this training for your team?

We run the training on your code and your domain problem. We agree the scope, dates and format with you.

Discuss the training