Exercise 09 · Domain vs. integration events and the broker
Application submission
- MortgageApplicationSubmitted
- domain vs integration events
- broker
Principle
Domain events (IncomeChanged…) belong to the aggregate and are internal. The integration event MortgageApplicationSubmitted is a stable contract for other services.
When internal events are exposed to the broker, every subscriber couples to their shape, and refactoring inside the calculator breaks other services.
That's why, on application submission, the calculator translates the domain event ApplicationSubmitted into a small integration event.
What to watch
- Internal events appear in the broker only in the anti-pattern.
- Risk Service ends up in the DLQ on IncomeChanged v2.
Without the solution / With the solution
Without the solution
Run with leaking domain events
The calculator publishes all domain events to the broker. Risk Service couples to IncomeChanged and fails after its shape changes.
Run in the simulator without the solutionWith the solution
Run with the integration event only
Only a single, versioned MortgageApplicationSubmitted event goes out. Internal changes in the calculator break no one.
Run in the simulator with the solutionWhat to do: Click “Run with leaking domain events”. What to look at: Risk Service receives a message it doesn't understand, and it ends up in the DLQ. 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.
Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
# Task for a coding agent: Application submission
Stack: Java. Mode: guided.
Goal: Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
## Context of the existing code
You are working in an existing project (Java backend (typically Java 21, Spring Boot, JPA or JDBC). Use the build tool and style the project already has (Maven/Gradle, JUnit 5).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store with a feed (subscription with a checkpoint) and a domain event `ApplicationSubmitted`. If not, first create a minimal “submit application” command on top of a completed calculation; without a broker, an in-memory `publish(topic, event)` is enough.
Read first, change nothing. Find and briefly summarise:
- What the calculator currently publishes to the broker/queue and who subscribes to it.
- Contracts (schemas) of existing messages.
- 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
Domain events are internal. Once they go to the broker, subscribers bind to their shape: after `IncomeChanged` moves to v2, Risk Service ends up in the DLQ. Only the versioned `MortgageApplicationSubmitted` goes out, e.g. `{ eventId, type, version: 1, occurredAt, correlationId, causationId, data: { applicationId, loanAmountMinor, monthlyPaymentMinor, monthlyIncomeMinor, termMonths } }`.
## Specific task
Technical specifics (Java): Mapper (MapStruct or by hand) to a `record`; verify the schema with a JSON Schema/Avro test; publish via the project's existing messaging layer.
Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: define the integration event (only the fields consumers need, version 1, correlationId).
2. Add a domain → integration event translation layer (anti-corruption).
3. Publish the integration event on ApplicationSubmitted from the feed of stored events, not from the command alongside the write; domain events never leave the service. Store the `correlationId` from the request header (otherwise a new one) in the domain event; `causationId` = the domain event id; `eventId` stays the same on retries.
4. Contract test: the v1 schema is pinned (changing the file fails the test); a change to the internal shape doesn't break it.
Watch out for:
- An integration event must not carry the whole aggregate.
- Send only the data the consumer needs and cannot derive itself.
## 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
- Only the integration event goes to the broker.
- Contract test on the schema.
- Correct correlationId and causationId.
- 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: Application submission
Stack: Java. Mode: challenge.
Goal: Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
## Context of the existing code
You are working in an existing project (Java backend (typically Java 21, Spring Boot, JPA or JDBC). Use the build tool and style the project already has (Maven/Gradle, JUnit 5).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store with a feed (subscription with a checkpoint) and a domain event `ApplicationSubmitted`. If not, first create a minimal “submit application” command on top of a completed calculation; without a broker, an in-memory `publish(topic, event)` is enough.
Read first, change nothing. Find and briefly summarise:
- What the calculator currently publishes to the broker/queue and who subscribes to it.
- Contracts (schemas) of existing messages.
- 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
Domain events are internal. Once they go to the broker, subscribers bind to their shape: after `IncomeChanged` moves to v2, Risk Service ends up in the DLQ. Only the versioned `MortgageApplicationSubmitted` goes out, e.g. `{ eventId, type, version: 1, occurredAt, correlationId, causationId, data: { applicationId, loanAmountMinor, monthlyPaymentMinor, monthlyIncomeMinor, termMonths } }`.
## Brief (Challenge)
On application submission, publish a versioned integration event translated from the domain event, and ensure with a test that internal events don't leak out.
Technical specifics (Java): Mapper (MapStruct or by hand) to a `record`; verify the schema with a JSON Schema/Avro test; publish via the project's existing messaging layer.
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
- Only the integration event goes to the broker.
- Contract test on the schema.
- Correct correlationId and causationId.
- 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: Application submission
Stack: C#. Mode: guided.
Goal: Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
## Context of the existing code
You are working in an existing project (C# backend (.NET, ASP.NET Core, EF Core or Dapper). Use the solution, style and DI conventions the project already has (xUnit/NUnit).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store with a feed (subscription with a checkpoint) and a domain event `ApplicationSubmitted`. If not, first create a minimal “submit application” command on top of a completed calculation; without a broker, an in-memory `publish(topic, event)` is enough.
Read first, change nothing. Find and briefly summarise:
- What the calculator currently publishes to the broker/queue and who subscribes to it.
- Contracts (schemas) of existing messages.
- 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
Domain events are internal. Once they go to the broker, subscribers bind to their shape: after `IncomeChanged` moves to v2, Risk Service ends up in the DLQ. Only the versioned `MortgageApplicationSubmitted` goes out, e.g. `{ eventId, type, version: 1, occurredAt, correlationId, causationId, data: { applicationId, loanAmountMinor, monthlyPaymentMinor, monthlyIncomeMinor, termMonths } }`.
## Specific task
Technical specifics (C#): Mapper to a `record`; verify the schema with a snapshot test; publish via the project's existing messaging library.
Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: define the integration event (only the fields consumers need, version 1, correlationId).
2. Add a domain → integration event translation layer (anti-corruption).
3. Publish the integration event on ApplicationSubmitted from the feed of stored events, not from the command alongside the write; domain events never leave the service. Store the `correlationId` from the request header (otherwise a new one) in the domain event; `causationId` = the domain event id; `eventId` stays the same on retries.
4. Contract test: the v1 schema is pinned (changing the file fails the test); a change to the internal shape doesn't break it.
Watch out for:
- An integration event must not carry the whole aggregate.
- Send only the data the consumer needs and cannot derive itself.
## 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
- Only the integration event goes to the broker.
- Contract test on the schema.
- Correct correlationId and causationId.
- 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: Application submission
Stack: C#. Mode: challenge.
Goal: Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
## Context of the existing code
You are working in an existing project (C# backend (.NET, ASP.NET Core, EF Core or Dapper). Use the solution, style and DI conventions the project already has (xUnit/NUnit).).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store with a feed (subscription with a checkpoint) and a domain event `ApplicationSubmitted`. If not, first create a minimal “submit application” command on top of a completed calculation; without a broker, an in-memory `publish(topic, event)` is enough.
Read first, change nothing. Find and briefly summarise:
- What the calculator currently publishes to the broker/queue and who subscribes to it.
- Contracts (schemas) of existing messages.
- 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
Domain events are internal. Once they go to the broker, subscribers bind to their shape: after `IncomeChanged` moves to v2, Risk Service ends up in the DLQ. Only the versioned `MortgageApplicationSubmitted` goes out, e.g. `{ eventId, type, version: 1, occurredAt, correlationId, causationId, data: { applicationId, loanAmountMinor, monthlyPaymentMinor, monthlyIncomeMinor, termMonths } }`.
## Brief (Challenge)
On application submission, publish a versioned integration event translated from the domain event, and ensure with a test that internal events don't leak out.
Technical specifics (C#): Mapper to a `record`; verify the schema with a snapshot test; publish via the project's existing messaging library.
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
- Only the integration event goes to the broker.
- Contract test on the schema.
- Correct correlationId and causationId.
- 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: Application submission
Stack: Web (HTML/JS or the app's stack). Mode: guided.
Goal: Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
## Context of the existing code
You are working in an existing project (web application: a browser frontend, possibly with its own backend. If the project has a backend, the event store, broker and services belong on the backend, and the points about endpoints and tables refer to it. If it has no backend, use IndexedDB in the browser and `fake-indexeddb` in tests. Keep the project's framework and test runner; add new dependencies only with a justification.).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store with a feed (subscription with a checkpoint) and a domain event `ApplicationSubmitted`. If not, first create a minimal “submit application” command on top of a completed calculation; without a broker, an in-memory `publish(topic, event)` is enough.
Read first, change nothing. Find and briefly summarise:
- What the calculator currently publishes to the broker/queue and who subscribes to it.
- Contracts (schemas) of existing messages.
- 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
Domain events are internal. Once they go to the broker, subscribers bind to their shape: after `IncomeChanged` moves to v2, Risk Service ends up in the DLQ. Only the versioned `MortgageApplicationSubmitted` goes out, e.g. `{ eventId, type, version: 1, occurredAt, correlationId, causationId, data: { applicationId, loanAmountMinor, monthlyPaymentMinor, monthlyIncomeMinor, termMonths } }`.
## Specific task
Technical specifics (Web (HTML/JS or the app's stack)): A `toIntegrationEvent(domainEvent)` function; verify the schema with a test using the project's validator (Zod, Ajv), otherwise with a manual shape check.
Proceed step by step. After point 1 (plan), write what you found and what you propose, and wait for confirmation:
1. Plan: define the integration event (only the fields consumers need, version 1, correlationId).
2. Add a domain → integration event translation layer (anti-corruption).
3. Publish the integration event on ApplicationSubmitted from the feed of stored events, not from the command alongside the write; domain events never leave the service. Store the `correlationId` from the request header (otherwise a new one) in the domain event; `causationId` = the domain event id; `eventId` stays the same on retries.
4. Contract test: the v1 schema is pinned (changing the file fails the test); a change to the internal shape doesn't break it.
Watch out for:
- An integration event must not carry the whole aggregate.
- Send only the data the consumer needs and cannot derive itself.
## 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
- Only the integration event goes to the broker.
- Contract test on the schema.
- Correct correlationId and causationId.
- 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: Application submission
Stack: Web (HTML/JS or the app's stack). Mode: challenge.
Goal: Separate internal domain events from integration events and publish only the stable MortgageApplicationSubmitted contract.
## Context of the existing code
You are working in an existing project (web application: a browser frontend, possibly with its own backend. If the project has a backend, the event store, broker and services belong on the backend, and the points about endpoints and tables refer to it. If it has no backend, use IndexedDB in the browser and `fake-indexeddb` in tests. Keep the project's framework and test runner; add new dependencies only with a justification.).
Domain: a mortgage calculator that is gradually turning into an event-driven system (calculator, risk, approval, identity, account, documents, payout).
Prerequisite: the project has an event store with a feed (subscription with a checkpoint) and a domain event `ApplicationSubmitted`. If not, first create a minimal “submit application” command on top of a completed calculation; without a broker, an in-memory `publish(topic, event)` is enough.
Read first, change nothing. Find and briefly summarise:
- What the calculator currently publishes to the broker/queue and who subscribes to it.
- Contracts (schemas) of existing messages.
- 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
Domain events are internal. Once they go to the broker, subscribers bind to their shape: after `IncomeChanged` moves to v2, Risk Service ends up in the DLQ. Only the versioned `MortgageApplicationSubmitted` goes out, e.g. `{ eventId, type, version: 1, occurredAt, correlationId, causationId, data: { applicationId, loanAmountMinor, monthlyPaymentMinor, monthlyIncomeMinor, termMonths } }`.
## Brief (Challenge)
On application submission, publish a versioned integration event translated from the domain event, and ensure with a test that internal events don't leak out.
Technical specifics (Web (HTML/JS or the app's stack)): A `toIntegrationEvent(domainEvent)` function; verify the schema with a test using the project's validator (Zod, Ajv), otherwise with a manual shape check.
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
- Only the integration event goes to the broker.
- Contract test on the schema.
- Correct correlationId and causationId.
- 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