Exercise 02 · Events as the source of truth, state = fold(events)

Calculator Event Sourcing

  • event stream
  • aggregate
  • current state

Principle

Instead of overwriting a row we record facts in the past tense: CalculationCreated, IncomeChanged, PropertyPriceChanged, RateChanged, TermChanged, CalculationCompleted.

The current state (aggregate) is not stored; it is produced by replaying events from the start of the stream: state = fold(events). A command is first validated against the state (invariant: instalment max. 45 % of income) and only then is an event created.

The event store is append-only. An event is never changed or deleted.

What to watch

  • Each change adds an event with the correct stream version.
  • The current state on the right always equals a replay of the whole stream.
  • The aggregate rejects completing a calculation with too high an instalment.

Without the solution / With the solution

Without the solution

Run the CRUD version

The same sequence of changes in a CRUD model: at the end only the last state remains and the question “how many times did the income change, and from what” has no answer.

Run in the simulator without the solution

With the solution

Run Event Sourcing

The same changes as events. History can be replayed, queried (“income changed 2×”) and explained. We don't need to know a new view in advance; we only need to keep the facts.

Run in the simulator with the solution

What to do: Click “Run CRUD version”. What to look at: After four changes only the last state remains and we don't know how many times the income changed. 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.

Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

Language
Variant
# Task for a coding agent: Calculator Event Sourcing

Stack: Java. Mode: guided.
Goal: Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

## 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).

Read first, change nothing. Find and briefly summarise:
- The calculator entity/model and all places that change it.
- The rules (invariants) currently checked on save.
- The existing calculations table/collection you don't want to break.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (event store), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Instead of overwriting a row, facts are recorded in the past tense; the current state (aggregate) is produced by replaying the stream: state = fold(events). A command is first validated against the state, and only then is an event created with the next stream version; an event is never changed or deleted. Example: income 80 000 → 90 000 → 100 000 CZK gives two `IncomeChanged` events and the history can be replayed and queried (“income changed 2×”). The aggregate rejects completing a calculation with an instalment above 45 % of income and writes nothing.

## Specific task

Technical specifics (Java): Immutable `record` for events, the aggregate as a plain class; event store via JdbcTemplate or JPA with a unique index.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: design the stream id and the payloads of the six events. Default proposal: `CalculationCreated`, `IncomeChanged`, `PropertyPriceChanged` (price, down payment), `RateChanged`, `TermChanged`, `CalculationCompleted` (calculation closed for submission).
2. Add an event store table/collection (streamId, version, type, schemaVersion, payload, timestamp) with a unique (streamId, version) and a global write order `position`.
3. Implement the aggregate: `decide(state, command) -> events` and `apply(state, event) -> state`, with no I/O.
4. Implement `load(streamId)` as a fold of events (returns state and version) and `append(streamId, expectedVersion, events)`.
5. Write tests in the given-events / when-command / then-events style, including rejection above the instalment limit (e.g. `CompleteCalculation` above 45 % of income). If the application checks the limit only at submission, put the rule in the command that closes the calculation; do not change the old endpoint's behaviour.
6. Wire the new aggregate alongside the old CRUD endpoint (e.g. `/api/v2/calculations`). Leave the old endpoint, table and frontend unchanged; do not migrate existing calculations.

Watch out for:
- `apply` must not make decisions or call I/O.
- Name events in the past tense and give them only facts, not computed values for the UI. Not even `CalculationCompleted` carries the instalment; that will later be covered by the calculation algorithm version.
- The unique index (streamId, version) is the basis of concurrency control (optimistic concurrency).

## 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

- The aggregate state is produced only from events.
- Given/when/then tests cover both success and a rejected command.
- The old endpoint works unchanged.
- 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: Calculator Event Sourcing

Stack: Java. Mode: challenge.
Goal: Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

## 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).

Read first, change nothing. Find and briefly summarise:
- The calculator entity/model and all places that change it.
- The rules (invariants) currently checked on save.
- The existing calculations table/collection you don't want to break.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (event store), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Instead of overwriting a row, facts are recorded in the past tense; the current state (aggregate) is produced by replaying the stream: state = fold(events). A command is first validated against the state, and only then is an event created with the next stream version; an event is never changed or deleted. Example: income 80 000 → 90 000 → 100 000 CZK gives two `IncomeChanged` events and the history can be replayed and queried (“income changed 2×”). The aggregate rejects completing a calculation with an instalment above 45 % of income and writes nothing.

## Brief (Challenge)

Add an event-sourced calculator aggregate alongside the existing CRUD storage, with six events, the instalment invariant and given/when/then tests.

Technical specifics (Java): Immutable `record` for events, the aggregate as a plain class; event store via JdbcTemplate or JPA with a unique index.

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

- The aggregate state is produced only from events.
- Given/when/then tests cover both success and a rejected command.
- The old endpoint works unchanged.
- 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: Calculator Event Sourcing

Stack: C#. Mode: guided.
Goal: Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

## 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).

Read first, change nothing. Find and briefly summarise:
- The calculator entity/model and all places that change it.
- The rules (invariants) currently checked on save.
- The existing calculations table/collection you don't want to break.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (event store), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Instead of overwriting a row, facts are recorded in the past tense; the current state (aggregate) is produced by replaying the stream: state = fold(events). A command is first validated against the state, and only then is an event created with the next stream version; an event is never changed or deleted. Example: income 80 000 → 90 000 → 100 000 CZK gives two `IncomeChanged` events and the history can be replayed and queried (“income changed 2×”). The aggregate rejects completing a calculation with an instalment above 45 % of income and writes nothing.

## Specific task

Technical specifics (C#): Events are `record` types, the aggregate a plain class with `Decide`/`Apply`; event store via EF Core/Dapper with a unique index.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: design the stream id and the payloads of the six events. Default proposal: `CalculationCreated`, `IncomeChanged`, `PropertyPriceChanged` (price, down payment), `RateChanged`, `TermChanged`, `CalculationCompleted` (calculation closed for submission).
2. Add an event store table/collection (streamId, version, type, schemaVersion, payload, timestamp) with a unique (streamId, version) and a global write order `position`.
3. Implement the aggregate: `decide(state, command) -> events` and `apply(state, event) -> state`, with no I/O.
4. Implement `load(streamId)` as a fold of events (returns state and version) and `append(streamId, expectedVersion, events)`.
5. Write tests in the given-events / when-command / then-events style, including rejection above the instalment limit (e.g. `CompleteCalculation` above 45 % of income). If the application checks the limit only at submission, put the rule in the command that closes the calculation; do not change the old endpoint's behaviour.
6. Wire the new aggregate alongside the old CRUD endpoint (e.g. `/api/v2/calculations`). Leave the old endpoint, table and frontend unchanged; do not migrate existing calculations.

Watch out for:
- `apply` must not make decisions or call I/O.
- Name events in the past tense and give them only facts, not computed values for the UI. Not even `CalculationCompleted` carries the instalment; that will later be covered by the calculation algorithm version.
- The unique index (streamId, version) is the basis of concurrency control (optimistic concurrency).

## 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

- The aggregate state is produced only from events.
- Given/when/then tests cover both success and a rejected command.
- The old endpoint works unchanged.
- 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: Calculator Event Sourcing

Stack: C#. Mode: challenge.
Goal: Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

## 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).

Read first, change nothing. Find and briefly summarise:
- The calculator entity/model and all places that change it.
- The rules (invariants) currently checked on save.
- The existing calculations table/collection you don't want to break.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (event store), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Instead of overwriting a row, facts are recorded in the past tense; the current state (aggregate) is produced by replaying the stream: state = fold(events). A command is first validated against the state, and only then is an event created with the next stream version; an event is never changed or deleted. Example: income 80 000 → 90 000 → 100 000 CZK gives two `IncomeChanged` events and the history can be replayed and queried (“income changed 2×”). The aggregate rejects completing a calculation with an instalment above 45 % of income and writes nothing.

## Brief (Challenge)

Add an event-sourced calculator aggregate alongside the existing CRUD storage, with six events, the instalment invariant and given/when/then tests.

Technical specifics (C#): Events are `record` types, the aggregate a plain class with `Decide`/`Apply`; event store via EF Core/Dapper with a unique index.

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

- The aggregate state is produced only from events.
- Given/when/then tests cover both success and a rejected command.
- The old endpoint works unchanged.
- 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: Calculator Event Sourcing

Stack: Web (HTML/JS or the app's stack). Mode: guided.
Goal: Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

## 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).

Read first, change nothing. Find and briefly summarise:
- The calculator entity/model and all places that change it.
- The rules (invariants) currently checked on save.
- The existing calculations table/collection you don't want to break.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (event store), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Instead of overwriting a row, facts are recorded in the past tense; the current state (aggregate) is produced by replaying the stream: state = fold(events). A command is first validated against the state, and only then is an event created with the next stream version; an event is never changed or deleted. Example: income 80 000 → 90 000 → 100 000 CZK gives two `IncomeChanged` events and the history can be replayed and queried (“income changed 2×”). The aggregate rejects completing a calculation with an instalment above 45 % of income and writes nothing.

## Specific task

Technical specifics (Web (HTML/JS or the app's stack)): Events as objects with `type` and `payload`, `apply` as a pure reducer function; event store alongside calculation persistence (a table on the backend, an IndexedDB object store without a backend) keyed by [streamId, version].

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: design the stream id and the payloads of the six events. Default proposal: `CalculationCreated`, `IncomeChanged`, `PropertyPriceChanged` (price, down payment), `RateChanged`, `TermChanged`, `CalculationCompleted` (calculation closed for submission).
2. Add an event store table/collection (streamId, version, type, schemaVersion, payload, timestamp) with a unique (streamId, version) and a global write order `position`.
3. Implement the aggregate: `decide(state, command) -> events` and `apply(state, event) -> state`, with no I/O.
4. Implement `load(streamId)` as a fold of events (returns state and version) and `append(streamId, expectedVersion, events)`.
5. Write tests in the given-events / when-command / then-events style, including rejection above the instalment limit (e.g. `CompleteCalculation` above 45 % of income). If the application checks the limit only at submission, put the rule in the command that closes the calculation; do not change the old endpoint's behaviour.
6. Wire the new aggregate alongside the old CRUD endpoint (e.g. `/api/v2/calculations`). Leave the old endpoint, table and frontend unchanged; do not migrate existing calculations.

Watch out for:
- `apply` must not make decisions or call I/O.
- Name events in the past tense and give them only facts, not computed values for the UI. Not even `CalculationCompleted` carries the instalment; that will later be covered by the calculation algorithm version.
- The unique index (streamId, version) is the basis of concurrency control (optimistic concurrency).

## 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

- The aggregate state is produced only from events.
- Given/when/then tests cover both success and a rejected command.
- The old endpoint works unchanged.
- 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: Calculator Event Sourcing

Stack: Web (HTML/JS or the app's stack). Mode: challenge.
Goal: Introduce an append-only event stream for the calculator aggregate and derive the current state from it.

## 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).

Read first, change nothing. Find and briefly summarise:
- The calculator entity/model and all places that change it.
- The rules (invariants) currently checked on save.
- The existing calculations table/collection you don't want to break.
- Which of these parts already exist in the project and which are missing. Before writing your own infrastructure (event store), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Instead of overwriting a row, facts are recorded in the past tense; the current state (aggregate) is produced by replaying the stream: state = fold(events). A command is first validated against the state, and only then is an event created with the next stream version; an event is never changed or deleted. Example: income 80 000 → 90 000 → 100 000 CZK gives two `IncomeChanged` events and the history can be replayed and queried (“income changed 2×”). The aggregate rejects completing a calculation with an instalment above 45 % of income and writes nothing.

## Brief (Challenge)

Add an event-sourced calculator aggregate alongside the existing CRUD storage, with six events, the instalment invariant and given/when/then tests.

Technical specifics (Web (HTML/JS or the app's stack)): Events as objects with `type` and `payload`, `apply` as a pure reducer function; event store alongside calculation persistence (a table on the backend, an IndexedDB object store without a backend) keyed by [streamId, version].

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

- The aggregate state is produced only from events.
- Given/when/then tests cover both success and a rejected command.
- The old endpoint works unchanged.
- 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