skip to content

learn.smmeyer.devsystem-design05

system-design/05

distributed core · core · ~4h practice

Queues, streams & async systems

outcome

Design durable asynchronous flows with explicit ordering, duplicate, retry, and backpressure behavior.

Plan a practice session · ~4h including exercises
Read + trace the worked example
30 min
Assignment + constraint variations
140 min
Timed drill + repeated explanations
50 min
Closed-book recall + self-review
20 min

This is a suggested practice budget, including drawing, variations, and repeated drills. Reading is the first block. Split the work across sessions and use the rubric to judge when to move on.

why this matters

Queues remove work from latency-critical paths, but they move complexity into time: lag, redelivery, poison messages, ordering, and reconciliation.

coverage

  • Queue vs log: destructive work distribution vs retained replayable history.
  • Kafka model: topics, partitions, consumer groups, offsets, ordering, retention.
  • Delivery: at-most-once, at-least-once, effectively-once, dedup keys.
  • Pressure: lag, bounded queues, load shedding, admission control.
  • Event systems: events vs commands, CQRS, event sourcing.
  • Jobs: delay, schedule, retry, dead-letter, priority, cancellation.

mental model

A reliable async flow separates acceptance, durable intent, execution, and outcome.

  1. 1 →Accept

    Persist intent

  2. 2 →Enqueue

    Partition + order

  3. 3 →Consume

    Idempotent work

  4. 4 →Record

    Outcome + retry

Read in numbered order: accept → enqueue → consume → record.

lessons

01Choose by semantics

A queue gives one worker ownership of a task. A log retains ordered partition history so multiple consumer groups can independently replay it. Kafka ordering is per partition—not global—so partition by the entity whose events must stay ordered.

A queue distributes work that should be processed, often allowing messages to disappear after acknowledgement. A durable log retains an ordered sequence that consumers track by position and can replay. Both can deliver asynchronously, but replay, retention, consumer independence, and ordering determine which fits. A notification send and a rebuildable analytics projection have different needs.

Follow one message through states: accepted, persisted, available, delivered, processing, effect committed, acknowledged. The producer must know what broker acceptance guarantees. The consumer must know when redelivery can occur. Ordering usually applies within a chosen partition or key, not across all events. If all events for an account must be ordered, map that account consistently and plan for hot accounts.

A schema is a long-lived contract because retained messages may be replayed after code changes. Include stable event identity, event time, schema version, and the data needed by the consumer. Decide whether an event records a historical fact or asks the receiver to read current state. Those meanings differ when the source has changed since the event was produced.

  • Events describe facts; commands request actions
  • Offsets are consumer progress, not business completion
  • Retention enables replay and new consumers

02At-least-once is the practical default

A worker can finish the side effect and crash before acknowledgment, so redelivery happens. Make handlers idempotent with a stable operation key, transactional state transition, or effect ledger.

A worker can commit a database update and crash before acknowledgement. Redelivery then repeats the work. If the effect and deduplication record share one transactional database, insert the message ID and perform the update atomically; a duplicate sees the existing ID and skips the effect. Merely setting a “processed” flag after the effect leaves a crash window.

External effects are harder. A sent email cannot be rolled back with your database. A provider idempotency key or status lookup can help reconcile an ambiguous send. Without either, document whether you accept possible duplicates or possible omission. A blanket “exactly once” label hides this boundary. Broker delivery guarantees do not automatically cover a remote business effect.

The transactional outbox solves a different gap: committing application state and recording the intent to publish. Write both in one transaction; a relay later publishes outbox rows. A relay crash after publish but before marking the row sent can produce another delivery, so consumers still need duplicate handling. The outbox prevents lost intent; it does not eliminate every duplicate.

  • Exactly-once claims have a system boundary
  • Retries need exponential backoff and jitter
  • Dead-letter queues need an owner and replay procedure

03Backpressure is part of the API

When arrival exceeds service rate, queues grow and latency becomes unbounded. Bound the queue, reject or defer low-priority work, shed optional load, and expose truthful status.

Queues absorb temporary imbalance, not unlimited overload. If producers add 1,000 jobs/s and workers finish 800, backlog grows by 200/s. After ten minutes that is 120,000 extra jobs. Even if throughput later returns to normal, you need spare capacity to drain them. Estimate waiting time from backlog and completion rate, and observe oldest-message age to connect backlog to user impact.

Set bounds at every stage: accepted work, in-flight messages per worker, retry attempts, message age, and dead-letter retention. Retry transient failures with delay; separate poison messages that will fail identically. In a strictly ordered partition, skipping a poison event may violate business order, while stopping blocks following events. Make that tradeoff explicit instead of silently moving it aside.

Backpressure communicates that the system cannot accept work at the promised pace. You can reject, defer, lower priority, or offer an asynchronous completion time. Expose truthful status so clients do not create duplicates by resubmitting. Protect high-priority jobs from a flood of bulk work through separate pools or scheduling policy, then test whether retries respect the same limits.

  • Track queue age, not only depth
  • Reserve capacity for high-priority tenants
  • Admission control belongs before scarce work

worked example

Survive a worker crash after sending an email

A worker consumes notification N, sends it, and crashes before acknowledging the queue message.

  1. The broker redelivers N. At-least-once delivery has worked as designed, but an unguarded consumer sends twice.
  2. Give the notification a durable business ID. Track attempts and provider message IDs, and use a provider idempotency feature where available.
  3. A database sent flag alone cannot atomically cover an external send. After an ambiguous provider timeout, reconcile status before blindly replaying.
  4. Use an outbox to atomically record notification intent with the originating transaction. Bound retries, route poison messages to a dead-letter path, and monitor oldest-message age.

Design decision: Separate durable intent, delivery attempts, and externally confirmed effects. Document where duplicates remain possible.

Limit: Broker transactions do not automatically include an email provider. Exactly-once claims must name the boundary they actually cover.

Teaching scenario · traffic, prices, and capacity values are illustrative.

assignment

Produce: An outbox schema, consumer state machine, and crash/replay matrix for notifications.

Paper, a text editor, or a spreadsheet is enough. Model access and paid services are optional.

  1. Trace failures before publish, after publish, before send, after send, and before acknowledgement.
  2. Assign ordering and partition keys; calculate catch-up time after a ten-minute consumer outage.
  3. Define retry limits, dedup retention, dead-letter ownership, and a safe replay procedure; run the timelines again with your revised design.

Change a constraint

  • Replace email with a payment provider and tighten the effect contract.
  • One poison message blocks an ordered partition; define the business consequence of skipping it.

Self-review rubric

  • The origin transaction cannot silently lose notification intent.
  • Every crash point has a replay outcome.
  • The external-side-effect ambiguity is explicitly acknowledged.

If a criterion is missing, revise that part of the artifact before rating your practice below.

Compare a sample artifact after your attempt

One defensible answer under these assumptions. Compare the reasoning and checks with your work; other designs can satisfy the same contract.

Notification intent must survive the originating transaction. The provider supports either an idempotency key or status reconciliation; without those, duplicate/omission risk must be surfaced.

outbox(id PK, tenant, notificationId, payloadVersion, createdAt)
attempt(notificationId, attemptId, providerKey, status, receipt)
Origin: commit business state + outbox row atomically
Relay: publish notificationId; duplicate publication is possible
Consumer: pending → sending → sent | unresolved | terminal-failure

Crash before send: retry pending work
Crash after provider commit: reconcile providerKey before another send
Crash after local sent record: duplicate delivery reads receipt, no send
Poison payload: bounded attempts → dead-letter + operator reason

The outbox repairs lost publication intent; it does not make the external send atomic with the database. A sent flag written after sending leaves an ambiguous crash window. Monitor age of oldest pending notification and retries. For payments, use stricter effect identity and reconciliation instead of assuming email's duplicate tolerance.

Check your artifact

  • A rolled-back originating transaction leaves no publishable intent.
  • Replaying the same notification ID after success performs no new send.
  • A poison event in an ordered partition has an explicit stop/skip decision and downstream consequence.

A tempting mistake: Acknowledging before the effect can lose work; acknowledging after it can redeliver. The consumer protocol must handle both boundaries deliberately.

cumulative practice

Relay · build and evolve a private file service

Project brief

Keep one design document for Relay, a tenant-aware file service. Start with 100k daily users, ten downloads per user/day, 10k uploads/day, 1 MB typical bodies, 30-day retention, and a 5× download peak. Files are private to workspaces. Treat these as fictional planning assumptions. Later chapters add previews, recovery, regional constraints, compute, and an assistant. Record each change rather than replacing the earlier version; the final review combines the whole packet. These activities share the chapter's existing practice budget.

Keep one project document across this track. Reconstruct earlier reasoning before opening notes, then revise the same artifact. Use part of the assignment and review budget for this work. If you enter mid-course, sketch the earlier decisions first using the linked chapters.

Add to your project: Relay's durable preview-generation pipeline and backlog calculation.

Add asynchronous previews after file publication. Recall the dual-write problem and use an outbox. In a burst of two preview jobs/s, four workers each finish 0.25 jobs/s. How fast does backlog grow, and what happens if a worker crashes after publishing a preview but before acknowledgement?

Compare cumulative reasoning after your attempt

Workers finish one job/s, so backlog grows by one job/s, or 600 jobs in ten minutes. Publish a versioned preview under stable file/job identity and record completion so redelivery does not create another visible version. Commit preview intent with file metadata through an outbox. Bound accepted preview work and expose pending status; adding a queue alone does not repair a sustained capacity deficit.

Keep your first attempt and revision together. If your answer differs, identify the assumption or mechanism that explains the difference before rating your recall.

senior signal

State the ordering unit and duplicate boundary. Those two sentences distinguish an operational design from boxes-and-arrows.

failure patterns

  • Saying Kafka provides global ordering
  • Calling an event ‘exactly once’ without naming the external effect boundary
  • Adding a DLQ without alerting or replay ownership
  • Allowing infinite queue growth to protect availability metrics

knowledge checks

Start with these quick checks, then explain the closed-book questions in your own words.

  1. 01Kafka guarantees order across…

    select an option to check your answer

  2. 02Best lag signal for user impact?

    select an option to check your answer

closed-book recall

Close the worked example and explain these from memory. Answers are self-reviewed. Drafts stay on this page; save anything you want to keep in chapter notes.

Rate the assignment and recall together

Use the assignment rubric as evidence. Completion is your own judgment; this score schedules practice.

interview drill

Design a notification platform for email, push, and in-app messages with priorities and user preferences.

  • 1B/day
  • at-least-once
  • quiet hours
  • provider outages
compare a strong approach after your attempt

Commit a notification intent, expand channels asynchronously, check preferences near send time, and enqueue per provider/priority. Use idempotency keys per notification-channel, provider circuit breakers, retry schedules, DLQs, and delivery receipts. Shed promotional work before transactional alerts.

Compare decisions and failure behavior, then repeat with a changed constraint. Keep your own wording.

Use the timed mock workspace →

references

Sources offer deeper context. Provider capabilities and API contracts can change; check current official docs before implementing.

notes

saved locally