Exercise 10 · RiskAssessmentCompleted/Rejected, MortgageApproved/Rejected, eventual consistency
Risk and approval
- Risk Service
- Approval
- eventual consistency
- read model
Principle
Risk Service reacts to MortgageApplicationSubmitted and emits RiskAssessmentCompleted or RiskAssessmentRejected. Approval Service then emits MortgageApproved or MortgageRejected.
The services do not talk to each other directly; each processes an event and emits the next one. The read model (projection) is updated only afterwards, so it can lag behind for a while: eventual consistency.
You don't remove it, you design for it: the UI shows “processing”, source of truth vs. read model, and after a write it doesn't read a stale view.
What to watch
- The state in the read model lags behind the events in the event store.
- Rejection variants: LTV 95 % (Risk) and a loan of 11 M (Approval).
Without the solution / With the solution
Without the solution
Run approval with a slow projection
The projection has a latency of 3.5 s. The client sees a stale state in the read model even though the application is already approved.
Run in the simulator without the solutionWith the solution
Run with a fast projection and a notice
The delay cannot disappear, but the UI shows it (a “Read model is behind” badge) and after a write it knows which version to expect. A faster projection shortens the inconsistency window.
Run in the simulator with the solutionWhat to do: Click “Run approval with a slow projection”. What to look at: The read model lags behind the actual state of the application for a while. 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 an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency.
# Task for a coding agent: Risk and approval Stack: Java. Mode: guided. Goal: Add an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency. ## 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 integration event `MortgageApplicationSubmitted`. If not, first create a minimal in-memory broker and publish this event when the application is submitted. Read first, change nothing. Find and briefly summarise: - Where risk calculation and approval happen today (a synchronous call?) and according to which rules. - How the UI finds out the application state (polling, push). - 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 Risk and Approval do not talk to each other directly; each processes an event and emits the next one. The read model is updated only afterwards, so it lags behind for a while; the UI shows “processing” and after a write it doesn't read a stale view. Rejection example: LTV 95 % is rejected by Risk, a loan of 11 M is rejected by Approval. ## Specific task Technical specifics (Java): Consumers via the existing messaging (Spring Cloud Stream/Kafka listener), read model in a separate table. Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation: 1. Plan: a list of events and who emits/consumes them (a producer → event → consumer table). 2. Risk handler: MortgageApplicationSubmitted → RiskAssessmentCompleted | RiskAssessmentRejected. If a rejection rule is missing, introduce one and state it in the summary. 3. Approval handler: RiskAssessmentCompleted → MortgageApproved | MortgageRejected; skip RiskAssessmentRejected. Output of both handlers: `eventId` derived from the input, `correlationId` unchanged, `causationId` = the input's `eventId`. 4. An application-state read model with a version (flow step: 1 submission, 2 risk, 3 approval) and a final-state flag; it ignores an older version. 5. UI: a “processing” state until the read model has the version from the submission or the state is final. Watch out for: - Read-your-own-writes: the user has just submitted the application and sees a stale state. - Don't send data that can go stale through events (application state, copies of another service's data); send the id and the fact of the decision. ## 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 approval path and both rejections have a test. - The read model carries the version and the final state, and the UI supports a “processing” state. - Handlers are pure with respect to the input event (the same event yields the same message, including `eventId`). - 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: Risk and approval Stack: Java. Mode: challenge. Goal: Add an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency. ## 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 integration event `MortgageApplicationSubmitted`. If not, first create a minimal in-memory broker and publish this event when the application is submitted. Read first, change nothing. Find and briefly summarise: - Where risk calculation and approval happen today (a synchronous call?) and according to which rules. - How the UI finds out the application state (polling, push). - 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 Risk and Approval do not talk to each other directly; each processes an event and emits the next one. The read model is updated only afterwards, so it lags behind for a while; the UI shows “processing” and after a write it doesn't read a stale view. Rejection example: LTV 95 % is rejected by Risk, a loan of 11 M is rejected by Approval. ## Brief (Challenge) Connect Risk and Approval as asynchronous event consumers and display the application state so that the UI doesn't lie during projection delay. Technical specifics (Java): Consumers via the existing messaging (Spring Cloud Stream/Kafka listener), read model in a separate table. 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 approval path and both rejections have a test. - The read model carries the version and the final state, and the UI supports a “processing” state. - Handlers are pure with respect to the input event (the same event yields the same message, including `eventId`). - 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: Risk and approval Stack: C#. Mode: guided. Goal: Add an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency. ## 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 integration event `MortgageApplicationSubmitted`. If not, first create a minimal in-memory broker and publish this event when the application is submitted. Read first, change nothing. Find and briefly summarise: - Where risk calculation and approval happen today (a synchronous call?) and according to which rules. - How the UI finds out the application state (polling, push). - 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 Risk and Approval do not talk to each other directly; each processes an event and emits the next one. The read model is updated only afterwards, so it lags behind for a while; the UI shows “processing” and after a write it doesn't read a stale view. Rejection example: LTV 95 % is rejected by Risk, a loan of 11 M is rejected by Approval. ## Specific task Technical specifics (C#): Consumers via the existing messaging layer (MassTransit/Service Bus, whatever the project uses), read model in a separate table. Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation: 1. Plan: a list of events and who emits/consumes them (a producer → event → consumer table). 2. Risk handler: MortgageApplicationSubmitted → RiskAssessmentCompleted | RiskAssessmentRejected. If a rejection rule is missing, introduce one and state it in the summary. 3. Approval handler: RiskAssessmentCompleted → MortgageApproved | MortgageRejected; skip RiskAssessmentRejected. Output of both handlers: `eventId` derived from the input, `correlationId` unchanged, `causationId` = the input's `eventId`. 4. An application-state read model with a version (flow step: 1 submission, 2 risk, 3 approval) and a final-state flag; it ignores an older version. 5. UI: a “processing” state until the read model has the version from the submission or the state is final. Watch out for: - Read-your-own-writes: the user has just submitted the application and sees a stale state. - Don't send data that can go stale through events (application state, copies of another service's data); send the id and the fact of the decision. ## 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 approval path and both rejections have a test. - The read model carries the version and the final state, and the UI supports a “processing” state. - Handlers are pure with respect to the input event (the same event yields the same message, including `eventId`). - 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: Risk and approval Stack: C#. Mode: challenge. Goal: Add an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency. ## 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 integration event `MortgageApplicationSubmitted`. If not, first create a minimal in-memory broker and publish this event when the application is submitted. Read first, change nothing. Find and briefly summarise: - Where risk calculation and approval happen today (a synchronous call?) and according to which rules. - How the UI finds out the application state (polling, push). - 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 Risk and Approval do not talk to each other directly; each processes an event and emits the next one. The read model is updated only afterwards, so it lags behind for a while; the UI shows “processing” and after a write it doesn't read a stale view. Rejection example: LTV 95 % is rejected by Risk, a loan of 11 M is rejected by Approval. ## Brief (Challenge) Connect Risk and Approval as asynchronous event consumers and display the application state so that the UI doesn't lie during projection delay. Technical specifics (C#): Consumers via the existing messaging layer (MassTransit/Service Bus, whatever the project uses), read model in a separate table. 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 approval path and both rejections have a test. - The read model carries the version and the final state, and the UI supports a “processing” state. - Handlers are pure with respect to the input event (the same event yields the same message, including `eventId`). - 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: Risk and approval Stack: Web (HTML/JS or the app's stack). Mode: guided. Goal: Add an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency. ## 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 integration event `MortgageApplicationSubmitted`. If not, first create a minimal in-memory broker and publish this event when the application is submitted. Read first, change nothing. Find and briefly summarise: - Where risk calculation and approval happen today (a synchronous call?) and according to which rules. - How the UI finds out the application state (polling, push). - 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 Risk and Approval do not talk to each other directly; each processes an event and emits the next one. The read model is updated only afterwards, so it lags behind for a while; the UI shows “processing” and after a write it doesn't read a stale view. Rejection example: LTV 95 % is rejected by Risk, a loan of 11 M is rejected by Approval. ## Specific task Technical specifics (Web (HTML/JS or the app's stack)): Polling or SSE on an application-state endpoint; display `version` and a “processing” state. Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation: 1. Plan: a list of events and who emits/consumes them (a producer → event → consumer table). 2. Risk handler: MortgageApplicationSubmitted → RiskAssessmentCompleted | RiskAssessmentRejected. If a rejection rule is missing, introduce one and state it in the summary. 3. Approval handler: RiskAssessmentCompleted → MortgageApproved | MortgageRejected; skip RiskAssessmentRejected. Output of both handlers: `eventId` derived from the input, `correlationId` unchanged, `causationId` = the input's `eventId`. 4. An application-state read model with a version (flow step: 1 submission, 2 risk, 3 approval) and a final-state flag; it ignores an older version. 5. UI: a “processing” state until the read model has the version from the submission or the state is final. Watch out for: - Read-your-own-writes: the user has just submitted the application and sees a stale state. - Don't send data that can go stale through events (application state, copies of another service's data); send the id and the fact of the decision. ## 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 approval path and both rejections have a test. - The read model carries the version and the final state, and the UI supports a “processing” state. - Handlers are pure with respect to the input event (the same event yields the same message, including `eventId`). - 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: Risk and approval Stack: Web (HTML/JS or the app's stack). Mode: challenge. Goal: Add an asynchronous Risk → Approval chain on top of integration events and design the UI for eventual consistency. ## 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 integration event `MortgageApplicationSubmitted`. If not, first create a minimal in-memory broker and publish this event when the application is submitted. Read first, change nothing. Find and briefly summarise: - Where risk calculation and approval happen today (a synchronous call?) and according to which rules. - How the UI finds out the application state (polling, push). - 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 Risk and Approval do not talk to each other directly; each processes an event and emits the next one. The read model is updated only afterwards, so it lags behind for a while; the UI shows “processing” and after a write it doesn't read a stale view. Rejection example: LTV 95 % is rejected by Risk, a loan of 11 M is rejected by Approval. ## Brief (Challenge) Connect Risk and Approval as asynchronous event consumers and display the application state so that the UI doesn't lie during projection delay. Technical specifics (Web (HTML/JS or the app's stack)): Polling or SSE on an application-state endpoint; display `version` and a “processing” state. 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 approval path and both rejections have a test. - The read model carries the version and the final state, and the UI supports a “processing” state. - Handlers are pure with respect to the input event (the same event yields the same message, including `eventId`). - 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.
Related parts of the presentation
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