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:

  1. {"type": "ShipOrder", "order_id": 889201}
  2. {"type": "OrderUpdated", "order_id": 889201, "changes": {"status": "shipped"}}
  3. {"type": "OrderShipped", "order_id": 889201, "version": 5, "carrier": "ups"}
  4. {"type": "OrderPlaced", "order_id": 889201, "order_total": 11600}
  5. {"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.

  1. Classify three event streams you know (or Kestrel's CDC stream, its order events, and a webhook).
  2. For each, say what it can and cannot replay.
  3. ยง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
  1. 24 lost of 10,000 at a 0.2% crash rate. Compute the annual loss at Kestrel's 6,575 orders a day.
  2. Change crash_rate to 0.02 and re-run. Is the relationship linear?
  3. The outbox produces 14 duplicates and loses 0. Explain why that is the better failure mode in one sentence.
  4. ยง36.5 says publishing first is worse. Explain the asymmetry.

Core

Exercise 36.4 โ€” Find your dual writes

Difficulty: โ˜…โ˜…โ˜† ยท Time: 45 minutes

  1. Search your codebase for a database write followed by a publish, an HTTP call, or a queue send โ€” outside a transaction.
  2. For each, estimate the crash window and the write volume. Compute the expected loss.
  3. Which of them has a reconciliation that would detect it? Check the independence of both sides (Case Study 1).
  4. 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

  1. Write the schema: outbox(id, aggregate_id, aggregate_type, event_type, payload, created_at, sent_at).
  2. Write the relay: read unsent, publish, mark sent. What happens if it crashes between publish and mark?
  3. Write the idempotent consumer, keyed on (aggregate_id, version).
  4. Test the duplicate path deliberately โ€” publish the same event twice and assert the projection is unchanged. Case Study 1's step 2.
  5. 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
  1. 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.
  2. Assert it against an independent path. The lab's state fold and revenue projection must agree; yours should have a similar cross-check.
  3. Now write one that a state table could not answer, and say precisely what information the state table has lost.
  4. ยง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.

  1. Take a real support question about a record in your system โ€” "why is this in this state?"
  2. Answer it from your current storage. How long did it take, and what could you not determine?
  3. Write what the event log for that aggregate would look like, and answer the same question from it.
  4. ยง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.

  1. Design the event types for an aggregate you know. Five or fewer.
  2. For each field, justify it against rule 1. Delete anything you cannot justify.
  3. Find the field that violates rule 2 โ€” there usually is one โ€” and remove it.
  4. ยง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
  1. List every projection or materialized view you maintain. For each, name the reducer.
  2. Mark each commutative or not, using ยง36.9's table.
  3. For the non-commutative ones: is it last-write-wins? If so, a version check suffices.
  4. Which ordering machinery do you currently pay for, and which of your projections needs it?
  5. 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.

  1. Replay the lab's four projections and confirm they are identical.
  2. Now break each of the three ways. Which does the lab's self-check catch, and which does it not?
  3. For a real projection you own: is it a pure function of its inputs? Grep it for network calls, now(), and random numbers.
  4. 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:

  1. Version-check dedup in the consumer, and assert it makes shuffled folds correct for the status projection. Case Study 2's Option B.
  2. A gap detector (Case Study 2) with a time window, distinguishing late from lost.
  3. An upcaster and a v0 event the current reducer cannot parse.
  4. A snapshot-and-truncate implementation (ยง36.12), asserting that folding from a snapshot plus subsequent events equals folding from the beginning.
  5. Crypto-shredding: encrypt payloads per subject, delete a key, and assert the projections still run while the customer's fields are unrecoverable.
  6. 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.

  1. List every reconciliation you run. For each, name both sides and their nearest common ancestor.
  2. Mark the ones whose sides share an ancestor. Those are not reconciliations.
  3. For your most important gold table, rank the available sources by independence and pick the most independent one that is available daily.
  4. Implement it. What is the objection, and what does it cost to price the objection away?
  5. 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.

  1. Audit your event payloads for personal data. How many event types carry it?
  2. ยง36.12's design decision is to carry customer_id rather than the address. How much of your log could be made pseudonymous by that rule?
  3. Choose between crypto-shredding and snapshot-and-truncate for one stream, and write the justification you would give counsel.
  4. What is your retention for raw events? What forced that number โ€” volume, cost, privacy, or nobody deciding?
  5. 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.

  1. Apply all four to a system you know. Be specific.
  2. ยง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.
  3. Kestrel event-sources orders and not the product catalog, and the catalog is the larger table. Explain why that is the right split.
  4. 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.

  1. Design the event types (ยง36.8). Five or fewer, past tense, versioned.
  2. Build the outbox, with the relay or CDC.
  3. Write the idempotent consumer base class and use it for every consumer.
  4. Write three projections, of which at least one must be non-commutative, and mark each.
  5. Add the version check for the non-commutative one, and prove it with a shuffled replay.
  6. Build the independent reconciliation (Case Study 1): the operational database against the platform, daily.
  7. Project every event into a queryable table (ยง36.7), and write the debugging query.
  8. Replay everything into a scratch schema and diff.
  9. 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.