System Design Masterclass · Module 15 / Week 3

Event-Driven Architecture & Messaging

Queues are buffers that convert latency problems into throughput problems — and coupling problems into consistency problems. Delivery guarantees as they actually are, RabbitMQ vs Kafka semantics, ordering, backpressure, and the DLQ discipline that keeps event estates debuggable.

~3h study~2h exercise1 interactive simFast track ~85 min

Fast track — running both brokers already?

Compresses to ~85 min. Motivation collapses; delivery-guarantee mechanics, the broker-semantics comparison, ordering, and failure handling stay mandatory — this module is your estate's native territory and its sharpest edges.

  1. §01 takeaway · 5 min
  2. §02 delivery sim, all three modes · 20 min · MANDATORY
  3. §03 semantics table, in full · 20 min · MANDATORY
  4. §04 ordering, in full · 15 min · MANDATORY
  5. §05 DLQ discipline, in full · 15 min · MANDATORY
STEP 01

What events buy — and cost

SKIM
Fast-track takeawayAsync messaging buys: temporal decoupling (consumer down ≠ producer blocked), load leveling (the queue absorbs the 21:00 spike, consumers drain at their pace — Module 06's buffer trade), fan-out without producer knowledge, and failure isolation. It costs: eventual consistency everywhere downstream (Modules 03–04 apply to every consumer), loss of request/response simplicity (correlation, sagas — M17), an ops-critical broker, and debuggability that must be designed (correlation IDs, tracing through the broker). The test for event vs RPC: does the producer need the answer to proceed? Yes → call. No → event.

The architectural upgrade events enable: inverting dependencies. "Purchase service calls entitlement, notification, analytics, recommendations" (knows 4 teams, breaks with each) becomes "purchase emits PurchaseCompleted; interested parties subscribe" — new consumers cost the producer nothing. That inversion is why event estates scale organizationally, and why their failure modes (this module) are worth mastering.

STEP 02

Delivery guarantees as they actually are

MANDATORY

Fast trackRun all three modes. The punchline to internalize: at-least-once is the only honest guarantee for work that matters, and it makes duplicates a certainty to design for, not an edge case.

10 events · consumer crashes mid-batch, then recovers
produced
processed
Ack timing decides everything: ack-before-process risks loss; process-before-ack risks duplicates. Pick your mode.
STEP 03

RabbitMQ vs Kafka: different machines, not competitors

MANDATORY
RabbitMQ (smart broker)Kafka (dumb broker, smart log)
Modelmessage queue: broker routes (exchanges/bindings), tracks per-message acks, deletes on ackpartitioned append-only log: consumers track their own offsets; messages retained by time/size regardless
Consumptioncompeting consumers per queue; per-message redeliveryconsumer groups: one consumer per partition per group; replay = rewind offset
Routingrich: topic/headers/fanout exchanges, per-queue TTLs, priorities, delaystopic + partition key; routing logic lives in producers/consumers
Replay / historynone once acked — a delivery systemnative — a record system: new consumers read from any point; reprocessing is a feature
Throughput shapeexcellent at task-queue scale; per-message bookkeeping caps itsequential-IO batched log: very high; built for firehoses
Durability knobsquorum queues (Raft — Module 03: PC/EC), publisher confirms, persistent messagesacks=all + min.insync.replicas=2 + producer idempotence — anything less can lose acked data on leader failover
Natural fit (your estate)commands & work distribution: transcoding jobs, notification dispatch, cache invalidation fan-out (M01)event streams & telemetry: playback QoE firehose, watch-history stream, analytics, CDC (M18)
Staff expectationThe selection question is "queue or log?" — does anyone need to re-read the past? Commands (do this once, then it's done) → queue. Facts (this happened; multiple presents and futures care) → log. Estates that put commands on Kafka fight rebalancing and per-message error handling; estates that put event streams on RabbitMQ rebuild retention and replay badly. Running both, each for its shape — as you do — is the correct answer, and the review-worthy skill is policing which shape each new use case is.
STEP 04

Ordering: scarce, local, and paid for

MANDATORY
STEP 05

Failure handling: the DLQ discipline

MANDATORY
STEP 06

Exercise

MANDATORY

Fast trackStep 1 (~40 min): producing the duplicate with your own crash teaches more than the whole §02 text.

1
Manufacture a duplicate. Rabbit consumer (manual acks) writing "entitlement granted" rows; System.exit(1) after the DB write, before the ack. Restart → redelivery → duplicate row. Repeat on Kafka with commit-after-process. You have now seen why Module 16 exists. Keep both duplicate rows as trophies.
2
Lose a message honestly. Kafka producer with acks=1; kill the partition leader right after a send; observe the acked-but-lost write with acks=all + min.insync.replicas=2 as the fix. Then break ordering: enable.idempotence=false, max.in.flight=5, inject a retry, watch per-key reorder; fix with idempotence on.
3
Build the retry ladder + DLQ. Rabbit: work queue → 5 s and 1 m delayed-retry queues (TTL+DLX) → parking-lot, with attempt-count headers and an alert on parking-lot depth. Poison a message and watch it walk the ladder without blocking the queue.
4
Paper. Audit five real topics/exchanges in your estate against this module: guarantee mode, ordering key + hot-key risk, retry/DLQ path, schema evolution policy, owner. The gaps become the platform backlog.

Self-check

Where exactly does Kafka's exactly-once stop applying?
At the boundary of the Kafka transaction: consume→transform→produce into Kafka topics with transactional producer + read_committed consumers is exactly-once in effect. The first non-Kafka side effect — a JPA write, an HTTP call — sits outside the transaction and can happen 0 or 2 times on crash/retry. Cross that boundary and you're back to at-least-once + idempotent effects (M16) or outbox-style patterns (M18).
Your consumer group rebalances every few minutes under backlog. Mechanism and fixes?
Processing a full max.poll.records batch exceeds max.poll.interval.ms → broker assumes the consumer died → rebalance → redelivery → more backlog: a self-sustaining loop. Fixes: smaller poll batches, longer poll interval, move heavy work off the polling thread (with manual offset management), and treat lag as a scaling signal before it becomes this.
Content updates must apply in order per title, but the consumer is 8-way parallel. Reconcile.
Partition by contentId (broker guarantees per-partition order), then preserve it inside the consumer: shard work across the 8 threads by hash(contentId) so each title's events stay on one thread — parallelism across titles, sequence within a title. Or sidestep: version-stamped full-state events + LWW make order irrelevant.
Why is a delayed-retry queue better than sleep-and-nack in the consumer?
Sleeping holds the consumer thread and its prefetch slot hostage — under a burst of failures the whole consumer stalls (and Rabbit immediate-redelivery nack just hot-loops the same message at the queue head). TTL+DLX delayed queues park the message in the broker, freeing the consumer for other work, with the backoff schedule explicit in topology and attempt-count in headers.