Quiz: Event-Driven Architecture

Twelve questions. Answers with explanations follow — work through them first.


1. Which of these is a well-formed event?

  • A. {"type": "ShipOrder", "order_id": 889201}
  • B. {"type": "OrderUpdated", "order_id": 889201, "changes": {...}}
  • C. {"type": "OrderShipped", "order_id": 889201, "version": 5, "carrier": "ups"}
  • D. {"type": "OrderPlaced", "order_id": 889201, "order_total": 11600}

2. §36.2 distinguishes three things called "event-driven." Which does CDC produce?

  • A. Event notification
  • B. Event-carried state transfer
  • C. Event sourcing
  • D. None of the three

3. A team uses the words event store, projection, and replay but has built event notification. What breaks eighteen months later?

  • A. Throughput
  • B. Somebody asks for a retroactive projection and the log cannot answer it, because the notifications never carried the data
  • C. Schema evolution
  • D. Consumer lag

4. The lab measures 24 events lost per 10,000 writes at a 0.2% crash rate. Why does no monitoring catch it?

  • A. The monitoring is misconfigured
  • B. No single component has both halves of the picture — the service knows it committed and is dead, and Kafka cannot know what it should have received
  • C. The loss rate is below the alerting threshold
  • D. The events are published asynchronously

5. §36.5 says publishing before committing is worse than committing before publishing. Why?

  • A. It is slower
  • B. A phantom event causes downstream work — the warehouse picks an order that does not exist — where a missing event merely under-reports
  • C. It breaks the retry logic
  • D. Kafka cannot roll back

6. The transactional outbox eliminates lost events and introduces duplicates. §36.5 calls this the right trade because:

  • A. Duplicates are rare
  • B. An idempotent consumer keyed on (aggregate_id, version) makes a duplicate a no-op, and no consumer design makes a missing event a no-op
  • C. Duplicates can be removed by compaction
  • D. The relay can detect them

7. The lab computes "time to ship" retroactively from existing events. §36.6 says that against a state table this projection is:

  • A. Slower
  • B. More expensive
  • C. Unanswerable — updated_at was overwritten when the order shipped, so the placement time is gone
  • D. Available with a self-join

8. §36.8 argues CDC is a poor way to publish domain events. Which of its three reasons does the chapter call the least visible?

  • A. A column rename becomes a breaking change for every consumer
  • B. Consumers receive normalized tables and must reconstruct the aggregate
  • C. There is no place for intent — a row going to cancelled does not say who cancelled it
  • D. CDC streams are higher volume

9. Shuffling the entire event stream leaves every sum exactly correct and corrupts 49.0% of order statuses. What does §36.9 conclude?

  • A. Ordering is never needed
  • B. You need per-aggregate ordering, not global — and for non-commutative projections a stored last_version_seen removes the need for ordering entirely
  • C. Sums should be recomputed nightly
  • D. Status projections should be avoided

10. In Case Study 2, partitioning by order_id and version checking in the consumer are described as:

  • A. Alternatives; pick one
  • B. Complementary — partitioning is a performance measure and version checking is the correctness property, which survives rebalances and replays
  • C. Both unnecessary given a global sequence number
  • D. Equivalent solutions with different costs

11. §36.11: the event log is 10× the state table and costs $0.27/month more. What does the chapter say the real costs are?

  • A. Storage and network egress
  • B. Engineering time — idempotent consumers forever, the outbox insert, projections that must be maintained and replayed, and debugging that becomes a fold
  • C. Query latency
  • D. Kafka licensing

12. §36.12: an immutable log meets an erasure request. Which response does the chapter call dishonest?

  • A. Crypto-shredding — encrypt per subject, delete the key
  • B. Snapshot and truncate
  • C. Rewriting the log — deleting or editing individual events
  • D. Keeping personal data out of the payload

---

Answers

1 — C.

A is imperative — ShipOrder is a command, which can be rejected; an event has already happened. B is a row update wearing an event's clothes: you cannot tell from OrderUpdated what happened or why, and it destroys the property that makes events useful. D violates §36.8's second rule by carrying a computed value — putting order_total in the event creates a second definition of the total (Chapter 30 §30.8). Only C is past tense, specific, versioned, and carries what the event is about.

2 — B. Event-carried state transfer.

CDC emits the new state of a row, so a consumer can act without calling back. This is what most "event-driven" data platforms actually run on. Note that CDC is a good way to replicate a database and a poor way to publish a domain event (§36.8), for three reasons of which the least visible is that a row change carries no intent.

3 — B. A retroactive projection the log cannot answer.

The notifications record when things changed, not what they changed to, so replaying them reconstructs today's state with yesterday's triggers, and §36.6's retroactive projections are unavailable. The architecture itself is fine; the vocabulary created expectations it cannot meet. The fix is free: call it what it is. A team that says "we use event notification" has an accurate shared model.

4 — B. No component has both halves of the picture.

The customer saw a confirmation, the service returned 200, Postgres committed, the load balancer recorded a 2xx, and the error log is empty — because the process that would have logged the error was dead. Kafka's absence of an event is not an error condition; there is no consumer waiting for that specific order. The only component that could detect it is one comparing Postgres to Kafka, and Case Study 1's team did not have one — which is a consequence rather than an oversight, since once the event stream is the source of truth, comparing it to the database feels like comparing the source to a copy.

5 — B. A phantom event causes work.

Committing first and crashing loses an event: the platform under-reports. Publishing first and crashing produces an event for an order that does not exist: the warehouse picks it, analytics count it, the customer is emailed about it. Under-reporting is bad; causing work on a nonexistent order is worse. Note also that retrying the publish helps and does not solve it — you cannot retry your way out of a crash between two non-atomic writes.

6 — B. An idempotent consumer makes a duplicate a no-op.

The choice is not between broken and perfect; it is between two failure modes. Duplication is loud if you look and fixable by the consumer; loss is silent, permanent, and unfixable, because the consumer never learns of it. This is the same asymmetry as at-least-once versus at-most-once delivery, and it resolves the same way. The version number is what makes dedup cheap — one comparison. The honest cost: every consumer must be idempotent forever, which is why Kestrel enforces it with a shared base class rather than a convention.

7 — C. Unanswerable.

Time to ship needs placed_at and shipped_at. A state table has status and updated_at, and updated_at was overwritten when the order shipped — the placement time is gone unless somebody anticipated the question and added a column. The removal-rate projection is worse: a line item added and then removed leaves no trace at all, because the row was deleted. Kestrel's operations team asked for p90 time to ship in a quarterly review; with events it took twenty minutes, and without them it would have required adding a column and waiting a quarter.

8 — C. There is no place for intent.

A column rename breaking consumers (A) is annoying and visible immediately. Reconstructing an aggregate (B) is work and is visible. Missing intent is invisible until somebody asks a question that needs it — and by then the history that would have carried it is years long. A CDC stream sees status change from placed to cancelled and cannot say whether the customer cancelled or the fraud system did, because the intent existed in the application and the write-ahead log never saw it.

9 — B. Per-aggregate ordering, and a version check for the rest.

The status projection needs version 7 to arrive after version 6 for the same order; it does not care about any other order. Partitioning by order_id gives that essentially free, and is dramatically cheaper than any global scheme. And a consumer storing last_version_seen and discarding anything not greater is correct under arbitrary reordering — six lines against a platform-wide guarantee. Case Study 2's team withdrew a two-quarter proposal on this basis. Note that A overstates it: sessionization is a state machine and genuinely needs watermarks; the answer differs by projection and by stream.

10 — B. Complementary.

Partitioning makes events arrive in order under normal operation, and does not survive a partition rebalance, a deliberate replay, or a producer retry. Version checking makes the consumer correct regardless of arrival order and does not care what the transport did. So partitioning reduces how often the version check discards anything, and the version check is what makes the projection right. A team that adopts only partitioning has a projection that is correct until the first rebalance — a failure that appears months later during an unrelated incident and gets attributed to it.

11 — B. Engineering time.

The storage objection is the one usually raised and is worth 27 cents a month at Kestrel's order volume: 2.4 million orders a year, 4.24 events each, ~420 bytes, three years — 30.5 million events and 13 GB, against a storage line that is 2.1% of the bill. Ten times a rounding error is a rounding error. The real costs are idempotent consumers forever including future ones, the outbox insert the application team must write, projections that are code and can be wrong since they were written, and debugging that becomes a fold rather than a SELECT. Argue the trade there — and note that clickstream at 14M events a day is a different conversation, because volume decides whether storage is in it.

12 — C. Rewriting the log.

Deleting or editing individual events breaks immutability, invalidates every version number after the deletion, and makes replay non-reproducible — and it is exactly what teams do under a statutory deadline when they have not planned for this. Crypto-shredding is the best answer and a substantial commitment; snapshot-and-truncate is honest and loses history before the boundary. The design decision that makes either available is keeping personal data out of the payload: carry customer_id, not the address, so erasure becomes a matter of the customer dimension and the log holds an identifier that no longer resolves.