You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In exercises 12-14 you built projections from a single event stream. Now you'll combine events from **multiple streams** into a single read model.
3
+
Build a multi-stream projection that correlates events from different streams by `PaymentId`.
4
4
5
-
## Scenario: Payment Verification
5
+
## Goal
6
6
7
-
A payment verification requires data from three independent checks, each producing events on its own stream:
7
+
Learn how to build projections that combine events from multiple event streams into a single read model.
8
8
9
-
1.**Payment recorded** — from the payment service (amount, order reference)
10
-
2.**Merchant limits checked** — from the merchant service (within daily limits?)
11
-
3.**Fraud score calculated** — from the fraud detection service (risk score, acceptable?)
12
-
4.**Verification completed** — final decision event (approved or rejected)
9
+
## Scenario
13
10
14
-
All events share a `PaymentId` that ties them to the same payment verification read model.
11
+
Events arrive from three different streams (payment, merchant, and fraud check), but they all reference the same `PaymentId`. Your projection must:
15
12
16
-
## What to implement
13
+
1. Collect data from all three event types
14
+
2. Store them in a single `PaymentVerification` read model
15
+
3. Derive the payment verification status when all data is present
17
16
18
-
With the [Database](./Tools/Database.cs) interface representing the sample database, implement a `PaymentVerification` read model and projection:
17
+
## Steps
19
18
20
-
1. Define the `PaymentVerification` read model properties — the test assertions tell you what shape it needs.
21
-
2. Create a `PaymentVerificationProjection` class with typed `Handle` methods for each event.
22
-
3. Register handlers in the test using `eventStore.Register`.
23
-
24
-
The key difference from single-stream projections: each event arrives on a **different stream ID**, but they all reference the same `PaymentId`. Your projection must use `PaymentId` (not the stream ID) as the read model key.
25
-
26
-
## Reference
27
-
28
-
Read more about multi-stream projections and handling events from multiple sources:
29
-
-[Handling Events Coming in an Unknown Order](https://www.architecture-weekly.com/p/handling-events-coming-in-an-unknown)
19
+
1. Create a `PaymentVerificationProjection` class with `Handle` methods for each event type
20
+
2. Register your handlers using `eventStore.Register`
21
+
3. Implement decision logic in the `FraudScoreCalculated` handler (always last for completed payments):
# Exercise 16 - Multi-Stream Projections with Out-of-Order Events
2
2
3
-
In exercise 15 you built multi-stream projections assuming events arrive in a predictable order. The real world isn't that kind. Events arrive **in any order**, especially when they come from different services.
3
+
Fix the projection from Exercise 15 to handle out-of-order events.
4
4
5
-
## Scenario: Payment Verification with Race Conditions
5
+
## Goal
6
6
7
-
The same payment verification domain, but now events arrive scrambled:
7
+
Learn how to build resilient projections that work even when events arrive in any order.
8
8
9
-
1.**FraudScoreCalculated** might arrive before **PaymentRecorded**
10
-
2.**MerchantLimitsChecked** could be first or last
11
-
3. No **PaymentVerificationCompleted** event — your projection derives the decision when enough data arrives
9
+
## Context
12
10
13
-
This teaches you to build **resilient read models** using the phantom record pattern.
11
+
Events can arrive out of order (e.g., from different RabbitMQ queues or Kafka topics). The projection from Exercise 15 was built assuming ordered events — run the test to see it fail.
14
12
15
-
## Key Differences from Exercise 15
13
+
For example, `FraudScoreCalculated` might fire before `PaymentRecorded`, meaning the Amount is 0 when you try to make the decision.
16
14
17
-
1.**No final decision event** — the projection determines approval/rejection based on available data
18
-
2.**Handle partial state** — the read model exists even with incomplete information
19
-
3.**Derive status** — when you have all required data, calculate the final status
15
+
**Emit event when payment verification is completed**.
20
16
21
-
## What to implement
17
+
## Decision Logic
22
18
23
-
With the [Database](./Tools/Database.cs) interface representing the sample database, implement a resilient `PaymentVerification` projection:
24
-
25
-
1. Define additional `PaymentVerification` properties to store data from each event — but design it to handle missing data
26
-
2. Create a `PaymentVerificationProjection` class with typed `Handle` methods for each event
27
-
3. Each handler should work even if other events haven't arrived yet
28
-
4. When enough data exists, derive the final `Status` (Approved/Rejected/Pending)
29
-
5. Register handlers in the test using `eventStore.Register`
30
-
31
-
The test will append events **in scrambled order** and verify your projection handles partial state correctly.
32
-
33
-
## Reference
34
-
35
-
Read more about handling out-of-order events and phantom records:
36
-
-[Dealing with Race Conditions in Event-Driven Architecture](https://www.architecture-weekly.com/p/dealing-with-race-conditions-in-event)
19
+
Only derive a final status when you have all three pieces of data (payment, merchant check, fraud check). Then apply the same rules as Exercise 15.
0 commit comments