Exercises: Event-Driven Architecture
Solutions and grading notes are in the instructor companion. Exercises marked ๐งช use
code/event_lab.py.
Warm-Up
Exercise 36.1 โ Event or row?
Difficulty: โ โโ ยท Time: 15 minutes
For each, say whether it is a well-formed event, and fix the ones that are not:
{"type": "ShipOrder", "order_id": 889201}{"type": "OrderUpdated", "order_id": 889201, "changes": {"status": "shipped"}}{"type": "OrderShipped", "order_id": 889201, "version": 5, "carrier": "ups"}{"type": "OrderPlaced", "order_id": 889201, "order_total": 11600}{"type": "CustomerEmailChanged", "customer_id": 903418, "old": "...", "new": "..."}
Two of these are the same mistake. Which, and what is it?
Exercise 36.2 โ Which of the three?
Difficulty: โ โโ ยท Time: 15 minutes
ยง36.2: event notification, event-carried state transfer, event sourcing.
- Classify three event streams you know (or Kestrel's CDC stream, its order events, and a webhook).
- For each, say what it can and cannot replay.
- ยง36.2's โ ๏ธ callout describes adopting one's vocabulary with another's design. Have you seen it? What expectation did it create?
Exercise 36.3 โ Read the outbox measurement
Difficulty: โ โโ ยท Time: 20 minutes ยท ๐งช
python code/event_lab.py --outbox
- 24 lost of 10,000 at a 0.2% crash rate. Compute the annual loss at Kestrel's 6,575 orders a day.
- Change
crash_rateto 0.02 and re-run. Is the relationship linear? - The outbox produces 14 duplicates and loses 0. Explain why that is the better failure mode in one sentence.
- ยง36.5 says publishing first is worse. Explain the asymmetry.
Core
Exercise 36.4 โ Find your dual writes
Difficulty: โ โ โ ยท Time: 45 minutes
- Search your codebase for a database write followed by a publish, an HTTP call, or a queue send โ outside a transaction.
- For each, estimate the crash window and the write volume. Compute the expected loss.
- Which of them has a reconciliation that would detect it? Check the independence of both sides (Case Study 1).
- Pick the worst one and write the outbox migration plan, including where idempotency has to land first.
Exercise 36.5 โ Build the outbox
Difficulty: โ โ โ ยท Time: 60 minutes
- Write the schema:
outbox(id, aggregate_id, aggregate_type, event_type, payload, created_at, sent_at). - Write the relay: read unsent, publish, mark sent. What happens if it crashes between publish and mark?
- Write the idempotent consumer, keyed on
(aggregate_id, version). - Test the duplicate path deliberately โ publish the same event twice and assert the projection is unchanged. Case Study 1's step 2.
- Now do it with CDC on the outbox table instead of a relay. What does that remove, and what does it add?
Exercise 36.6 โ Build a retroactive projection
Difficulty: โ โ โ ยท Time: 45 minutes ยท ๐งช
python code/event_lab.py --project
- The lab has four projections. Write a fifth โ for example, the rate at which shipped orders are later refunded, or revenue by SKU including removals.
- Assert it against an independent path. The lab's state fold and revenue projection must agree; yours should have a similar cross-check.
- Now write one that a state table could not answer, and say precisely what information the state table has lost.
- ยง36.6 claims "unanswerable is stronger than slow." Test the claim โ is your projection genuinely impossible against a state table, or merely expensive?
Exercise 36.7 โ The debugging query
Difficulty: โ โ โ ยท Time: 30 minutes
ยง36.7: the benefit that survives a change of team.
- Take a real support question about a record in your system โ "why is this in this state?"
- Answer it from your current storage. How long did it take, and what could you not determine?
- Write what the event log for that aggregate would look like, and answer the same question from it.
- ยง36.7's warning: if the log is not queryable by ordinary people, you have the architecture and not the benefit. Who at your organization could run the query?
Exercise 36.8 โ Design an event schema
Difficulty: โ โ โ ยท Time: 45 minutes
ยง36.8's three rules: carry what the event is about ยท never carry a computed value a consumer could compute differently ยท version and never break.
- Design the event types for an aggregate you know. Five or fewer.
- For each field, justify it against rule 1. Delete anything you cannot justify.
- Find the field that violates rule 2 โ there usually is one โ and remove it.
- ยง36.8 argues CDC is a poor way to publish domain events, with three reasons. Which of the three applies most to your system?
Exercise 36.9 โ Sort your projections by commutativity
Difficulty: โ โ โ ยท Time: 40 minutes ยท ๐งช
python code/event_lab.py --order
- List every projection or materialized view you maintain. For each, name the reducer.
- Mark each commutative or not, using ยง36.9's table.
- For the non-commutative ones: is it last-write-wins? If so, a version check suffices.
- Which ordering machinery do you currently pay for, and which of your projections needs it?
- Case Study 2's PR-template question: write yours.
Exercise 36.10 โ Replay
Difficulty: โ โ โ ยท Time: 60 minutes ยท ๐งช
ยง36.10: three real replay failures โ an unhandled old event type, a projection that calls an external service, and a schema version nobody remembered.
- Replay the lab's four projections and confirm they are identical.
- Now break each of the three ways. Which does the lab's self-check catch, and which does it not?
- For a real projection you own: is it a pure function of its inputs? Grep it for network calls,
now(), and random numbers. - Design the quarterly replay-and-diff. Where does it write, and what does it compare?
Advanced
Exercise 36.11 โ Extend the lab
Difficulty: โ โ โ ยท Time: 90 minutes ยท ๐งช
Add three to event_lab.py, with self-checks:
- Version-check dedup in the consumer, and assert it makes shuffled folds correct for the status projection. Case Study 2's Option B.
- A gap detector (Case Study 2) with a time window, distinguishing late from lost.
- An upcaster and a v0 event the current reducer cannot parse.
- A snapshot-and-truncate implementation (ยง36.12), asserting that folding from a snapshot plus subsequent events equals folding from the beginning.
- Crypto-shredding: encrypt payloads per subject, delete a key, and assert the projections still run while the customer's fields are unrecoverable.
- A compaction simulator showing that compacting an event-sourced topic destroys it.
Then: #4's assertion โ snapshot + tail == full fold โ is the most valuable one in the file. Say why.
Exercise 36.12 โ Reconcile against something independent
Difficulty: โ โ โ ยท Time: 60 minutes
Case Study 1's finding: two derived numbers agreeing tells you nothing about the source.
- List every reconciliation you run. For each, name both sides and their nearest common ancestor.
- Mark the ones whose sides share an ancestor. Those are not reconciliations.
- For your most important gold table, rank the available sources by independence and pick the most independent one that is available daily.
- Implement it. What is the objection, and what does it cost to price the objection away?
- Case Study 1's independent check cost a
count(*)on a read replica. What does yours cost?
Exercise 36.13 โ Retention and erasure
Difficulty: โ โ โ ยท Time: 60 minutes
ยง36.12: an immutable log and an erasure obligation are in direct conflict.
- Audit your event payloads for personal data. How many event types carry it?
- ยง36.12's design decision is to carry
customer_idrather than the address. How much of your log could be made pseudonymous by that rule? - Choose between crypto-shredding and snapshot-and-truncate for one stream, and write the justification you would give counsel.
- What is your retention for raw events? What forced that number โ volume, cost, privacy, or nobody deciding?
- Confirm that no event-sourced topic in your system is compacted. If one is, what has already been lost?
Exercise 36.14 โ Argue against event sourcing
Difficulty: โ โ โ ยท Time: 45 minutes
ยง36.13 gives four conditions under which it is wrong.
- Apply all four to a system you know. Be specific.
- ยง36.11 says the storage objection is not an objection and lists the real costs. Price the real costs for your system, in engineering time.
- Kestrel event-sources orders and not the product catalog, and the catalog is the larger table. Explain why that is the right split.
- Construct the system where partial adoption is worse than either extreme.
Project Milestone
Exercise 36.15 โ Event-source one aggregate
Difficulty: โ โ โ ยท Time: 4โ5 hours ยท ๐งช
Pick one aggregate in the Kestrel platform โ orders is the obvious choice โ and event-source it.
- Design the event types (ยง36.8). Five or fewer, past tense, versioned.
- Build the outbox, with the relay or CDC.
- Write the idempotent consumer base class and use it for every consumer.
- Write three projections, of which at least one must be non-commutative, and mark each.
- Add the version check for the non-commutative one, and prove it with a shuffled replay.
- Build the independent reconciliation (Case Study 1): the operational database against the platform, daily.
- Project every event into a queryable table (ยง36.7), and write the debugging query.
- Replay everything into a scratch schema and diff.
- Write the retention position (ยง36.12), including what happens to an erasure request.
Deliverable: the event schemas, the outbox, the consumer base class, three projections with their commutativity marked, the reconciliation, the replay diff, and the one-paragraph retention position.
Step 8 is the one that will fail, and its failure is the deliverable. Record what it found โ a projection that was not pure, an event type nobody handled, or a schema version you had forgotten.