Exercise 06 · Two advisers, one calculation

Optimistic concurrency

  • expectedVersion
  • konflikt
  • invariant

Principle

Two users load the same aggregate version. Adviser B raises the property price, adviser A (with the old state) completes the calculation.

Without a version check both writes go through and the result is a calculation that breaks the instalment limit.

Optimistic concurrency: the write carries expectedVersion. If the stream has changed in the meantime, the write is rejected and the client loads the new version and decides again.

What to watch

  • The version each adviser has loaded.
  • Version conflict message vs. a silent write with a broken invariant.

Without the solution / With the solution

Without the solution

Run concurrent writes without a version check

Both advisers load version 1. B changes the price to 9.5 M. A then completes the calculation on the stale state.

Run in the simulator without the solution

With the solution

Run with optimistic concurrency

The same concurrent writes, but A's write is rejected. Client A loads the new version, sees the instalment above the limit and decides differently.

Run in the simulator with the solution

What to do: Click “Run concurrent writes without a version check”. What to look at: The second adviser completes the calculation on a stale version and exceeds the instalment limit. 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.

Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

Language
Variant
# Task for a coding agent: Optimistic concurrency

Stack: Java. Mode: guided.
Goal: Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

## 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 a calculation event store (Event Sourcing) with writes via `append`. If not, first create a minimal one: append-only events with a stream version.

Read first, change nothing. Find and briefly summarise:
- The place where events are written, who calls `append` (including in tests), and whether a unique index (streamId, version) exists.
- How the API returns errors (the code for a conflict) and whether 409 already means something else.
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (version check, command retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Two advisers load version 1. B changes the property price to 9.5 M (version 2), then A completes the calculation on the stale state; without a version check, both writes go through and the instalment exceeds the limit. So the write carries the expected version: A's completion with version 1 is rejected with the current version 2, and A decides again after loading the new version.

## Specific task

Technical specifics (Java): Unique index + catching `DuplicateKeyException`; test with `ExecutorService` and Testcontainers.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: where the client gets the version and how it sends it back (ETag/If-Match or a field in the body).
2. `append(streamId, expectedVersion, events)` compares the last version with `expectedVersion` in a single transaction (including one higher than the stream has) and writes nothing on a mismatch; the unique index is a safeguard. If other people's tests rely on the old signature, keep it.
3. The API returns 409 Conflict with the current version and `details.code = "version-conflict"` (with `If-Match`, HTTP prescribes 412; if you choose it, say so).
4. Policy: automatic retry only by reloading the state and running `decide` again, never by writing the old events, and never for a command carrying a client-supplied version; otherwise, return an error to the user.
5. Concurrency test: two clients write with the same version, exactly one succeeds. A synchronous handler won't interleave the requests: use a client-supplied version or a test double that inserts a write between `load` and `append`.

Watch out for:
- A “read the version, then write” check without atomicity is just a race in slow motion.
- Automatic retry can break an invariant.

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

- A concurrent write against the same version succeeds only once; a retry after a conflict does not break the invariant.
- The API returns 409 (or the chosen 412) and the current version; the UI offers to load the new version.
- The test runs against a real database, not just a mock (app without a DB: a built-in one, e.g. `node:sqlite`).
- 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: Optimistic concurrency

Stack: Java. Mode: challenge.
Goal: Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

## 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 a calculation event store (Event Sourcing) with writes via `append`. If not, first create a minimal one: append-only events with a stream version.

Read first, change nothing. Find and briefly summarise:
- The place where events are written, who calls `append` (including in tests), and whether a unique index (streamId, version) exists.
- How the API returns errors (the code for a conflict) and whether 409 already means something else.
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (version check, command retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Two advisers load version 1. B changes the property price to 9.5 M (version 2), then A completes the calculation on the stale state; without a version check, both writes go through and the instalment exceeds the limit. So the write carries the expected version: A's completion with version 1 is rejected with the current version 2, and A decides again after loading the new version.

## Brief (Challenge)

Introduce optimistic concurrency on writes to the calculator stream, including a 409 response and a test of two concurrent writes.

Technical specifics (Java): Unique index + catching `DuplicateKeyException`; test with `ExecutorService` and Testcontainers.

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

- A concurrent write against the same version succeeds only once; a retry after a conflict does not break the invariant.
- The API returns 409 (or the chosen 412) and the current version; the UI offers to load the new version.
- The test runs against a real database, not just a mock (app without a DB: a built-in one, e.g. `node:sqlite`).
- 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: Optimistic concurrency

Stack: C#. Mode: guided.
Goal: Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

## 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 a calculation event store (Event Sourcing) with writes via `append`. If not, first create a minimal one: append-only events with a stream version.

Read first, change nothing. Find and briefly summarise:
- The place where events are written, who calls `append` (including in tests), and whether a unique index (streamId, version) exists.
- How the API returns errors (the code for a conflict) and whether 409 already means something else.
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (version check, command retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Two advisers load version 1. B changes the property price to 9.5 M (version 2), then A completes the calculation on the stale state; without a version check, both writes go through and the instalment exceeds the limit. So the write carries the expected version: A's completion with version 1 is rejected with the current version 2, and A decides again after loading the new version.

## Specific task

Technical specifics (C#): Unique index + catching `DbUpdateException`; test with `Task.WhenAll`.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: where the client gets the version and how it sends it back (ETag/If-Match or a field in the body).
2. `append(streamId, expectedVersion, events)` compares the last version with `expectedVersion` in a single transaction (including one higher than the stream has) and writes nothing on a mismatch; the unique index is a safeguard. If other people's tests rely on the old signature, keep it.
3. The API returns 409 Conflict with the current version and `details.code = "version-conflict"` (with `If-Match`, HTTP prescribes 412; if you choose it, say so).
4. Policy: automatic retry only by reloading the state and running `decide` again, never by writing the old events, and never for a command carrying a client-supplied version; otherwise, return an error to the user.
5. Concurrency test: two clients write with the same version, exactly one succeeds. A synchronous handler won't interleave the requests: use a client-supplied version or a test double that inserts a write between `load` and `append`.

Watch out for:
- A “read the version, then write” check without atomicity is just a race in slow motion.
- Automatic retry can break an invariant.

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

- A concurrent write against the same version succeeds only once; a retry after a conflict does not break the invariant.
- The API returns 409 (or the chosen 412) and the current version; the UI offers to load the new version.
- The test runs against a real database, not just a mock (app without a DB: a built-in one, e.g. `node:sqlite`).
- 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: Optimistic concurrency

Stack: C#. Mode: challenge.
Goal: Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

## 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 a calculation event store (Event Sourcing) with writes via `append`. If not, first create a minimal one: append-only events with a stream version.

Read first, change nothing. Find and briefly summarise:
- The place where events are written, who calls `append` (including in tests), and whether a unique index (streamId, version) exists.
- How the API returns errors (the code for a conflict) and whether 409 already means something else.
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (version check, command retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Two advisers load version 1. B changes the property price to 9.5 M (version 2), then A completes the calculation on the stale state; without a version check, both writes go through and the instalment exceeds the limit. So the write carries the expected version: A's completion with version 1 is rejected with the current version 2, and A decides again after loading the new version.

## Brief (Challenge)

Introduce optimistic concurrency on writes to the calculator stream, including a 409 response and a test of two concurrent writes.

Technical specifics (C#): Unique index + catching `DbUpdateException`; test with `Task.WhenAll`.

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

- A concurrent write against the same version succeeds only once; a retry after a conflict does not break the invariant.
- The API returns 409 (or the chosen 412) and the current version; the UI offers to load the new version.
- The test runs against a real database, not just a mock (app without a DB: a built-in one, e.g. `node:sqlite`).
- 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: Optimistic concurrency

Stack: Web (HTML/JS or the app's stack). Mode: guided.
Goal: Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

## 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 a calculation event store (Event Sourcing) with writes via `append`. If not, first create a minimal one: append-only events with a stream version.

Read first, change nothing. Find and briefly summarise:
- The place where events are written, who calls `append` (including in tests), and whether a unique index (streamId, version) exists.
- How the API returns errors (the code for a conflict) and whether 409 already means something else.
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (version check, command retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Two advisers load version 1. B changes the property price to 9.5 M (version 2), then A completes the calculation on the stale state; without a version check, both writes go through and the instalment exceeds the limit. So the write carries the expected version: A's completion with version 1 is rejected with the current version 2, and A decides again after loading the new version.

## Specific task

Technical specifics (Web (HTML/JS or the app's stack)): Compare the version in the same transaction as the write, wherever the event store lives (backend or IndexedDB); return a conflict as an error and offer to load the new version.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: where the client gets the version and how it sends it back (ETag/If-Match or a field in the body).
2. `append(streamId, expectedVersion, events)` compares the last version with `expectedVersion` in a single transaction (including one higher than the stream has) and writes nothing on a mismatch; the unique index is a safeguard. If other people's tests rely on the old signature, keep it.
3. The API returns 409 Conflict with the current version and `details.code = "version-conflict"` (with `If-Match`, HTTP prescribes 412; if you choose it, say so).
4. Policy: automatic retry only by reloading the state and running `decide` again, never by writing the old events, and never for a command carrying a client-supplied version; otherwise, return an error to the user.
5. Concurrency test: two clients write with the same version, exactly one succeeds. A synchronous handler won't interleave the requests: use a client-supplied version or a test double that inserts a write between `load` and `append`.

Watch out for:
- A “read the version, then write” check without atomicity is just a race in slow motion.
- Automatic retry can break an invariant.

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

- A concurrent write against the same version succeeds only once; a retry after a conflict does not break the invariant.
- The API returns 409 (or the chosen 412) and the current version; the UI offers to load the new version.
- The test runs against a real database, not just a mock (app without a DB: a built-in one, e.g. `node:sqlite`).
- 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: Optimistic concurrency

Stack: Web (HTML/JS or the app's stack). Mode: challenge.
Goal: Protect stream writes with an expected-version check and handle the conflict in both the API and the UI.

## 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 a calculation event store (Event Sourcing) with writes via `append`. If not, first create a minimal one: append-only events with a stream version.

Read first, change nothing. Find and briefly summarise:
- The place where events are written, who calls `append` (including in tests), and whether a unique index (streamId, version) exists.
- How the API returns errors (the code for a conflict) and whether 409 already means something else.
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (version check, command retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

Two advisers load version 1. B changes the property price to 9.5 M (version 2), then A completes the calculation on the stale state; without a version check, both writes go through and the instalment exceeds the limit. So the write carries the expected version: A's completion with version 1 is rejected with the current version 2, and A decides again after loading the new version.

## Brief (Challenge)

Introduce optimistic concurrency on writes to the calculator stream, including a 409 response and a test of two concurrent writes.

Technical specifics (Web (HTML/JS or the app's stack)): Compare the version in the same transaction as the write, wherever the event store lives (backend or IndexedDB); return a conflict as an error and offer to load the new 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

- A concurrent write against the same version succeeds only once; a retry after a conflict does not break the invariant.
- The API returns 409 (or the chosen 412) and the current version; the UI offers to load the new version.
- The test runs against a real database, not just a mock (app without a DB: a built-in one, e.g. `node:sqlite`).
- 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