~learn.

Messaging and Async Processing, on one page.

Prints on a single A4 or Letter page. Download PDF opens your browser's print dialog: choose “Save as PDF”.

System Design Mastery · World 03 cheat sheet

Messaging and Async Processing

~/himanshufree course · himanshukukreja.in/learn

Day 1Queue vs Stream

Work to do vs things that happened. A queue holds jobs: each message goes to one worker and is deleted after the ack. A stream is an append-only log: events stay, each reader tracks its own offset and can replay.

Consumer groups. Kafka's model: every group gets all messages (fan-out) and within a group each partition goes to exactly one consumer (load balancing). Parallelism equals the partition count.

Order lives in the partition. Kafka orders messages only within a partition. Key by the entity that needs order (order_id, user_id) so "created, paid, shipped" can't be processed out of order.

At-least-once plus idempotent consumers. Exactly-once needs special support on both sides, so most systems ack after processing and dedupe, e.g. by partition and offset stored in the same transaction as the work.

6–12partitions for a new topic; 50–100+ for high-volume ones
9,000/slag growth when producing 10,000/s and consuming 1,000/s: 32.4M behind in an hour
7+ trillionmessages a day at LinkedIn, where Kafka was born
acks=allproducer setting for durability
Trade-off · Queue vs stream
Queue (RabbitMQ, SQS)Stream (Kafka, Kinesis)
After readingDeletedRetained
Consumers per messageOneEvery group
ReplayNoYes, rewind the offset
Use forJobs, tasks, webhooksEvent sourcing, analytics, fan-out
  • Kafka for simple background jobs that a queue with built-in retry would handle.
  • No partition key, so one order's events land on different partitions.
  • One partition "to start": only one consumer can ever work on it.

Day 2Transactional Outbox

The dual-write problem. Saving an order then publishing to Kafka are two writes with no shared transaction. A crash in between leaves an order no downstream service hears about; flipping them publishes events for orders that never saved.

Write the event to the database. Insert the business row and an outbox row in the same transaction: both commit or neither does. A separate publisher reads unpublished outbox rows and sends them to Kafka.

Polling or CDC. A poller queries published_at IS NULL every few seconds: simple, but adds latency and database load. CDC (Debezium) tails the write-ahead log: ~10–100 ms latency, no polling, more to operate.

Still at-least-once. A publisher that crashes after Kafka acks but before marking the row published sends it again. Consumers dedupe by event ID; Kafka transactions can't cover the database update.

~10–100 msDebezium CDC from INSERT to Kafka
> 10K events/sor < 1 s latency: move from polling to CDC
10,000 · 5 minalert thresholds: unpublished rows, oldest unpublished age
7 dayskeep published rows, then delete them
Trade-off · Polling publisher vs CDC
PollingCDC (Debezium)
LatencyUp to the poll intervalMilliseconds
Database loadExtra queriesReads the WAL
SetupAny database, no extra partsWAL access, Kafka Connect
Start withLow volumeHigh volume, low latency
  • Writing the outbox row in a separate transaction: the dual write is back.
  • Publishing to Kafka inside the request path, so Kafka trouble fails user requests.
  • No alert on outbox lag: the table grows to 10M rows before anyone notices.

Day 3Backpressure and Flow Control

Async removes natural backpressure. A synchronous client can't send faster than the server answers. A queue absorbs the gap instead, until memory runs out: absorption, then resource exhaustion, then collapse.

Watch lag, depth and latency. Consumer lag (alert at 10,000 messages or 5 minutes), queue depth (100,000 or growing for 5 minutes) and queue wait time. Throughput falling while lag grows means consumers are struggling.

Four responses. Shed low-priority load (metrics, logs), rate-limit producers (adaptively, as lag grows), scale consumers (Kafka: up to the partition count), or spill overflow to disk or S3 for later.

You get two of three. Completeness, latency, stability: pick two. Most systems keep completeness and stability and accept higher latency during spikes; real-time pricing drops stale events instead.

Recover gradually. After an outage, don't jump from 100 to 10,000 messages a second: the backlog floods downstream. Step up (500, 1,000, 2,500, 5,000, 10,000) after a minute of stability each.

5,000 vs 200,000/sdesign rate vs the spike that took a site down
12 partitionsat most 12 Kafka consumers
~3.5 hours10M bulk emails at 800/s
30 sage at which surge-pricing events are dropped
Trade-off · Backpressure strategies
Data lossLatencyBest for
Load sheddingYes, controlledNoneMetrics, logs
Rate limitingNoProducers waitAPI traffic
ScalingNoMinimalSustained load
SpilloverNoHigher for overflowMust-keep messages
  • Unbounded in-memory queues: the OOM killer takes every message with it.
  • Rate limiting only at the API gateway while internal services overwhelm the database.
  • Hot partition keys: one consumer drowns while the others idle.

Day 4Dead Letters and Poison Pills

A poison pill blocks everything behind it. A message that crashes the consumer every time causes a crash loop: restart, fetch the same message, crash. In the lesson, one bad message held 50,000 orders for 4 hours.

Classify the failure. Transient (timeouts, rate limits): retry with backoff. Permanent (bad JSON, missing field, rule violation): straight to the DLQ. Poison pills (crashes, hangs, memory blowups): quarantine immediately.

Delayed retry topics. Instead of retrying in place, republish to retry topics with growing delays (1 s, 10 s, 60 s), then the DLQ. The main topic keeps flowing; Uber described this design for Kafka.

A DLQ is an operational tool. Store the original message, error and stack trace, retry count, timestamps and source topic, partition and offset. Monitor depth, growth and age, triage daily, and build replay tooling from day one.

Replay needs idempotency. Check the payment_id before reprocessing so a replay can't double-charge. Old messages may not match today's schema: version them, migrate on replay, or close them out.

3–5 retrieswith exponential backoff: the default
0 retriesfor validation errors
2xnormal processing time as the poison-pill timeout
7 · 90 dayshot DLQ retention, then cold archive
Trade-off · How many retries?
Too few (1–2)Too many (10+)
Transient errorsLand in the DLQ needlesslyRecover
Poison pillsQuarantined fastBlock the queue longer
CostMore manual replaysWasted retries, slower detection
  • Resetting the retry count to 0 in each consumer instead of reading it from headers: infinite loop.
  • A DLQ nobody monitors: it becomes a graveyard of lost data.
  • A DLQ consumer that processes instead of just storing and alerting.

Day 5Audit Log System

Who did what, to which resource, when, from where. An audit log serves auditors, legal and security, not engineers: strict schema, retention in years, and it must be immutable. An application log is for debugging and can be deleted.

Capture in the same transaction. Write the audit event to an outbox in the same transaction as the business action, so a transfer can't succeed without its record. Kafka, consumers and the DLQ carry it asynchronously.

Immutable and tamper-evident. No UPDATE or DELETE: corrections are new events. Each event carries the previous event's hash, checkpointed somewhere an attacker can't change, and cold files sit under S3 Object Lock.

Tiered storage. Hot (0–90 days) in TimescaleDB for fast investigation queries, warm (to 1 year) in ClickHouse or BigQuery, cold (1–7 years) as Parquet on S3, queryable with Athena.

Never shed audit events. When consumers fall behind, scale them, batch harder and alert, but accept latency: audit data can't be dropped. Sort investigations by ingestion time and keep both timestamps.

50,000/speak audit events to design for (5,000 normally)
7 yearsretention for SOX-related financial records
90 days · 1 yearhot, then warm storage boundaries
< 100 msclock drift target, kept by NTP
Trade-off · Synchronous vs asynchronous audit
SynchronousAsynchronous (outbox)
User latency+50–200 ms+0 ms
VisibilityImmediate1–60 s
RiskAudit DB down, app downAudit lags during issues
  • Writing the audit event after the business transaction: money moves with no trail.
  • "Fixing" audit rows with UPDATE, which breaks the chain of custody.
  • Logging only "delete doc_123" without actor, context or reason.

BossEvent-Driven Food Delivery Order System

Size the dinner rush. 500,000 orders a day, 60% in 6–9 PM: ~28 orders/s, ~56 at the peak, design for 100. With ~8 events per order and 7 consumer services, that's 5,600 event deliveries a second.

Kafka keyed by order_id. Seven services need the same events, replay helps debugging, and each order's events must stay in order. Partition orders.events (32 partitions) by order_id; each service is its own consumer group.

Different consumers, different guarantees. Payment needs effectively-once with an idempotency key per order (Stripe won't charge twice). Analytics can lag, push is fire-and-forget, fraud detection needs real time.

Retry topics, then the DLQ. Transient failures go to retry topics delayed 1, 5 and 15 minutes; after 3 retries, the DLQ. Payment failures retry then notify the customer; restaurant rejections find an alternative or refund.

Audit for disputes. 2% of orders are disputed. Every service writes immutable audit events, so support queries one order's full timeline in minutes instead of correlating logs across services.

100 orders/sdesign target (peak ~56)
5,600/sevent deliveries: 800 events/s × 7 consumers
10K · 50K · 100Kconsumer lag: warning, critical, emergency
2 yearsaudit retention: ~5.8 TB of audit data
Trade-off · Responses by pressure level
Warning (10K lag)Critical (50K)Emergency (100K)
ScalePre-warm consumersAuto-scaleMax replicas
ShedNothingLow priorityNormal and low
PeopleSlack alertPage on-callRate-limit the API
  • Saving the order and publishing to Kafka as two writes: lost or phantom orders.
  • Shedding driver-assignment events: a late driver hurts more than late analytics.
  • Retrying forever in the main topic, so one failing order blocks the rest.