Exercise 12 · MortgageAccountOpeningRequested → MortgageAccountOpened, Inbox

Mortgage Account: duplicates and idempotency

  • at-least-once
  • duplicity
  • idempotence
  • Inbox

Principle

The broker guarantees “at least once”, not “exactly once”. The same message may arrive twice (lost acknowledgement, redelivery, retry).

Without idempotency the service opens a second account. An idempotent receiver remembers the processed eventIds (Inbox) and, for the same request, creates no new effect but returns the original result.

The Inbox is written in the same transaction as the effect; otherwise a window for duplicates opens again.

What to watch

  • The number of accounts in the service's local database.
  • The “duplicate-ignored” trace in the broker timeline.

Without the solution / With the solution

Without the solution

Run with duplicate delivery

The broker delivers each account-opening request 2×. The service has no Inbox.

Run in the simulator without the solution

With the solution

Run with Inbox

The service recognises the second delivery by its eventId and ignores it. A single account is created.

Run in the simulator with the solution

What to do: Click “Run with duplicate delivery”. What to look at: Mortgage Account opens two accounts for one mortgage. 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.

Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

Language
Variant
# Task for a coding agent: Mortgage Account: duplicates and idempotency

Stack: Java. Mode: guided.
Goal: Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

## 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 broker and the `MortgageAccountOpeningRequested` message (`applicationId`, `identityCheckId`) from Customer Identity after identity verification. If not, first create a minimal producer and consumer.

Read first, change nothing. Find and briefly summarise:
- The handler that opens the account, and the transaction boundary.
- Whether an inbox/deduplication already exists in the messaging library (e.g. the inbox in MassTransit/Wolverine).
- The natural business key (applicationId).
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (broker client, outbox, retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

The broker delivers “at least once”: the same message may arrive twice and without idempotency a second account is created. The idempotent receiver writes `eventId` to the Inbox in the same transaction as the account and ignores the second delivery. Example: a duplicate gets the original account (success, not 409) and the `MortgageAccountOpened` reply with the same `eventId`.

## Specific task

Technical specifics (Java): `@Transactional` handler, unique index on `message_id` and `application_id`.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: where the deduplication key (eventId) is and where the business key (applicationId) is.
2. An Inbox table (unique messageId) in the account service's database, written in the same transaction as the account creation. Over the network, send `eventId` as the idempotency key.
3. A duplicate returns the original result and republishes the reply event (example above).
4. A unique constraint on the business key as a second layer of defence. If the existing table can't take it, put it in a new table in the same transaction.
5. Test: the same message 2× → one account; two different requests for the same application → one account; a race that fails without the unique key (two database connections, or a double with a stale read).

Watch out for:
- “If exists” deduplication without a unique constraint has a race.
- The Inbox grows: a clean-up plan after retention; retention must outlast retries, the DLQ and manual redrive.

## 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 test for both duplicate and concurrent delivery.
- The Inbox and the effect are in one transaction (test: a failed write to the inbox leaves no account).
- An existing library is used if available.
- 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: Mortgage Account: duplicates and idempotency

Stack: Java. Mode: challenge.
Goal: Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

## 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 broker and the `MortgageAccountOpeningRequested` message (`applicationId`, `identityCheckId`) from Customer Identity after identity verification. If not, first create a minimal producer and consumer.

Read first, change nothing. Find and briefly summarise:
- The handler that opens the account, and the transaction boundary.
- Whether an inbox/deduplication already exists in the messaging library (e.g. the inbox in MassTransit/Wolverine).
- The natural business key (applicationId).
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (broker client, outbox, retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

The broker delivers “at least once”: the same message may arrive twice and without idempotency a second account is created. The idempotent receiver writes `eventId` to the Inbox in the same transaction as the account and ignores the second delivery. Example: a duplicate gets the original account (success, not 409) and the `MortgageAccountOpened` reply with the same `eventId`.

## Brief (Challenge)

Guarantee that repeated delivery of MortgageAccountOpeningRequested never creates a second account, even with concurrent delivery.

Technical specifics (Java): `@Transactional` handler, unique index on `message_id` and `application_id`.

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 test for both duplicate and concurrent delivery.
- The Inbox and the effect are in one transaction (test: a failed write to the inbox leaves no account).
- An existing library is used if available.
- 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: Mortgage Account: duplicates and idempotency

Stack: C#. Mode: guided.
Goal: Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

## 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 broker and the `MortgageAccountOpeningRequested` message (`applicationId`, `identityCheckId`) from Customer Identity after identity verification. If not, first create a minimal producer and consumer.

Read first, change nothing. Find and briefly summarise:
- The handler that opens the account, and the transaction boundary.
- Whether an inbox/deduplication already exists in the messaging library (e.g. the inbox in MassTransit/Wolverine).
- The natural business key (applicationId).
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (broker client, outbox, retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

The broker delivers “at least once”: the same message may arrive twice and without idempotency a second account is created. The idempotent receiver writes `eventId` to the Inbox in the same transaction as the account and ignores the second delivery. Example: a duplicate gets the original account (success, not 409) and the `MortgageAccountOpened` reply with the same `eventId`.

## Specific task

Technical specifics (C#): `DbContext` transaction, unique indexes; use the existing messaging library's inbox if it has one.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: where the deduplication key (eventId) is and where the business key (applicationId) is.
2. An Inbox table (unique messageId) in the account service's database, written in the same transaction as the account creation. Over the network, send `eventId` as the idempotency key.
3. A duplicate returns the original result and republishes the reply event (example above).
4. A unique constraint on the business key as a second layer of defence. If the existing table can't take it, put it in a new table in the same transaction.
5. Test: the same message 2× → one account; two different requests for the same application → one account; a race that fails without the unique key (two database connections, or a double with a stale read).

Watch out for:
- “If exists” deduplication without a unique constraint has a race.
- The Inbox grows: a clean-up plan after retention; retention must outlast retries, the DLQ and manual redrive.

## 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 test for both duplicate and concurrent delivery.
- The Inbox and the effect are in one transaction (test: a failed write to the inbox leaves no account).
- An existing library is used if available.
- 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: Mortgage Account: duplicates and idempotency

Stack: C#. Mode: challenge.
Goal: Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

## 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 broker and the `MortgageAccountOpeningRequested` message (`applicationId`, `identityCheckId`) from Customer Identity after identity verification. If not, first create a minimal producer and consumer.

Read first, change nothing. Find and briefly summarise:
- The handler that opens the account, and the transaction boundary.
- Whether an inbox/deduplication already exists in the messaging library (e.g. the inbox in MassTransit/Wolverine).
- The natural business key (applicationId).
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (broker client, outbox, retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

The broker delivers “at least once”: the same message may arrive twice and without idempotency a second account is created. The idempotent receiver writes `eventId` to the Inbox in the same transaction as the account and ignores the second delivery. Example: a duplicate gets the original account (success, not 409) and the `MortgageAccountOpened` reply with the same `eventId`.

## Brief (Challenge)

Guarantee that repeated delivery of MortgageAccountOpeningRequested never creates a second account, even with concurrent delivery.

Technical specifics (C#): `DbContext` transaction, unique indexes; use the existing messaging library's inbox if it has one.

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 test for both duplicate and concurrent delivery.
- The Inbox and the effect are in one transaction (test: a failed write to the inbox leaves no account).
- An existing library is used if available.
- 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: Mortgage Account: duplicates and idempotency

Stack: Web (HTML/JS or the app's stack). Mode: guided.
Goal: Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

## 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 broker and the `MortgageAccountOpeningRequested` message (`applicationId`, `identityCheckId`) from Customer Identity after identity verification. If not, first create a minimal producer and consumer.

Read first, change nothing. Find and briefly summarise:
- The handler that opens the account, and the transaction boundary.
- Whether an inbox/deduplication already exists in the messaging library (e.g. the inbox in MassTransit/Wolverine).
- The natural business key (applicationId).
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (broker client, outbox, retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

The broker delivers “at least once”: the same message may arrive twice and without idempotency a second account is created. The idempotent receiver writes `eventId` to the Inbox in the same transaction as the account and ignores the second delivery. Example: a duplicate gets the original account (success, not 409) and the `MortgageAccountOpened` reply with the same `eventId`.

## Specific task

Technical specifics (Web (HTML/JS or the app's stack)): With a backend, a single account-service transaction across the inbox and the account; without a backend, a single IndexedDB transaction across the inbox and the data.

Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: where the deduplication key (eventId) is and where the business key (applicationId) is.
2. An Inbox table (unique messageId) in the account service's database, written in the same transaction as the account creation. Over the network, send `eventId` as the idempotency key.
3. A duplicate returns the original result and republishes the reply event (example above).
4. A unique constraint on the business key as a second layer of defence. If the existing table can't take it, put it in a new table in the same transaction.
5. Test: the same message 2× → one account; two different requests for the same application → one account; a race that fails without the unique key (two database connections, or a double with a stale read).

Watch out for:
- “If exists” deduplication without a unique constraint has a race.
- The Inbox grows: a clean-up plan after retention; retention must outlast retries, the DLQ and manual redrive.

## 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 test for both duplicate and concurrent delivery.
- The Inbox and the effect are in one transaction (test: a failed write to the inbox leaves no account).
- An existing library is used if available.
- 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: Mortgage Account: duplicates and idempotency

Stack: Web (HTML/JS or the app's stack). Mode: challenge.
Goal: Make the MortgageAccountOpeningRequested consumer idempotent via an Inbox table in the same transaction as the effect.

## 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 broker and the `MortgageAccountOpeningRequested` message (`applicationId`, `identityCheckId`) from Customer Identity after identity verification. If not, first create a minimal producer and consumer.

Read first, change nothing. Find and briefly summarise:
- The handler that opens the account, and the transaction boundary.
- Whether an inbox/deduplication already exists in the messaging library (e.g. the inbox in MassTransit/Wolverine).
- The natural business key (applicationId).
- Which of these parts already exist in the project and which are missing. Before you write your own infrastructure (broker client, outbox, retry), check whether the project or organisation already has it as a ready-made library, and use it.

## Expected behaviour

The broker delivers “at least once”: the same message may arrive twice and without idempotency a second account is created. The idempotent receiver writes `eventId` to the Inbox in the same transaction as the account and ignores the second delivery. Example: a duplicate gets the original account (success, not 409) and the `MortgageAccountOpened` reply with the same `eventId`.

## Brief (Challenge)

Guarantee that repeated delivery of MortgageAccountOpeningRequested never creates a second account, even with concurrent delivery.

Technical specifics (Web (HTML/JS or the app's stack)): With a backend, a single account-service transaction across the inbox and the account; without a backend, a single IndexedDB transaction across the inbox and the data.

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 test for both duplicate and concurrent delivery.
- The Inbox and the effect are in one transaction (test: a failed write to the inbox leaves no account).
- An existing library is used if available.
- 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