Vasu Patel logoVasu Patel
← Designing at Scale: System Design
All chapters in this track
  1. 01Chapter 1: Why System Design Is Its Own Skill
  2. 02Chapter 2: Core Building Blocks and Vocabulary
  3. 03Chapter 3: Eliciting Non-Functional Requirements
  4. 04Chapter 4: Back-of-Envelope Capacity Estimation
  5. 05Chapter 5: Caching Strategies and Cache Invalidation at the Systems Level
  6. 06Chapter 6: Load Balancing and Horizontal Scaling
  7. 07Chapter 7: Database Selection, Replication, and Sharding at a Systems Level
  8. 08Chapter 8: Asynchronous Processing, Message Queues, and Event-Driven Design
  9. 09Chapter 9: API Design from a Systems Perspective: REST, gRPC, GraphQL
  10. 10Chapter 10: The CAP Theorem and Consistency Models in Practical Terms
  11. 11Chapter 11: Microservices vs. Monoliths and Drawing Service Boundaries
  12. 12Chapter 12: Designing for Failure: Redundancy, Failover, Graceful Degradation
  13. 13Chapter 13: A Repeatable Framework for Any System Design Problem
  14. 14Chapter 14: Worked Example: URL Shortener
  15. 15Chapter 15: Worked Example: Chat System
  16. 16Chapter 16: Worked Example: News Feed
  17. 17Chapter 17: Worked Example: Rate Limiter

Chapter 8 of 17

Chapter 8: Asynchronous Processing, Message Queues, and Event-Driven Design

Why this matters

Not every piece of work a system does needs to happen within a user's request/response cycle — recognizing which work can (and should) be moved off that critical path is one of the highest-leverage system design decisions available, directly improving perceived latency (Chapter 3) and system resilience simultaneously.

Identifying work that belongs off the synchronous path

[BEGINNER] The test: does the user need the result of this work immediately, in this response, to proceed? If not, it's a candidate for asynchronous processing.

Synchronous (must complete before responding): validating the request,
checking inventory, charging the customer's card, creating the order record.

Asynchronous (can happen after the response is already sent): sending a
confirmation email, generating a PDF receipt, updating a recommendation
model based on this purchase, notifying a shipping partner's system.

If email-sending were done SYNCHRONOUSLY, a slow email provider would
directly add its own latency to the user's checkout experience, even
though the user doesn't need to WAIT for the email to be sent in order
to see "order confirmed" on their screen.

The message queue's systems-level role: decoupling and absorbing load spikes

[SENIOR INSIGHT] A message queue (companion OS book, Chapter 8's IPC coverage explains the low-level mechanism; this chapter covers the systems-design purpose) sits between the producer of asynchronous work (the checkout service, after accepting an order) and the consumer that actually performs it (an email-sending worker) — providing two distinct systems-level benefits: decoupling (the checkout service doesn't need to know anything about how emails are sent, or even that an email service exists — it just publishes "order created" and moves on) and load absorption (a sudden burst of orders queues up work for the email workers to process at their own sustainable pace, rather than the burst directly overwhelming an email-sending service that can't handle a sudden spike).

Checkout Service --publishes "order.created" event--> Message Queue
                                                              |
                                    +-------------------------+-------------------------+
                                    v                                                     v
                        Email Worker (sends confirmation)          Analytics Worker (updates
                                                                     recommendation data)

Checkout Service has ALREADY responded to the user by this point --
none of this downstream processing affects the user's perceived latency.

Choosing between a simple queue and a full pub/sub event system

[IN PRACTICE] A simple queue (one producer, work distributed among a pool of consumers, each message processed by exactly one consumer) fits straightforward "do this work once" scenarios (send this specific email). A pub/sub system (a message published once, delivered to every subscriber independently) fits scenarios where multiple, independent parts of the system all need to react to the same event (Chapter 5's cross-service cache invalidation is exactly this case) — the systems design decision is recognizing which shape a given asynchronous need actually has: "exactly one consumer should handle this" versus "every interested consumer should independently react to this."

At-least-once delivery and idempotency: a systems-level correctness requirement

[SENIOR INSIGHT] Most real message queue systems provide at-least-once delivery — a message might, in rare failure scenarios (a worker crashing after processing but before acknowledging), be delivered and processed more than once. This has a direct, critical systems design consequence: any consumer of a queued message must be idempotent — processing the same message twice must produce the same result as processing it once, not a duplicated effect.

NOT idempotent (dangerous): "charge the customer's card for $50" --
processed twice, the customer is charged $100.

Idempotent (safe): "ensure the customer has been charged $50 for
ORDER-1234" -- check first whether ORDER-1234 has already been
successfully charged (directly the companion Concurrency book's Chapter
15 payment-idempotency pattern) before charging again -- processing
this message twice produces the SAME end state as processing it once.

This is a systems-level correctness requirement that must be designed in from the start for any asynchronous processing involving an effect that would be harmful if duplicated (charging money, sending a notification, decrementing inventory) — retrofitting idempotency after discovering a duplicate-processing bug in production is far more painful than designing for it upfront.

Common mistakes

  1. Keeping work synchronous that doesn't need to be, unnecessarily adding a downstream dependency's latency (and failure risk) directly to the user's perceived response time.
  2. Choosing a simple queue when the actual need is "every interested party should react independently" (or vice versa), producing either missed notifications (pub/sub need served by a simple queue, where only one consumer gets each message) or unnecessary duplicated processing (simple queue need served by pub/sub, where every subscriber redundantly does the same work).
  3. Not designing consumers to be idempotent, exposing the system to real, if rare, duplicate-processing bugs (double-charging, duplicate notifications) under at-least-once delivery's occasional redelivery.
  4. Moving genuinely core, user-needed work to an asynchronous path, ironically making the user experience worse (e.g., the user sees "order confirmed" before the payment has actually been verified, encountering a confusing failure later) — not all work belongs off the synchronous path; the test in this chapter's opening section needs to be applied correctly in both directions.

Discussion / interview questions

  1. "How would you decide which parts of a request's processing should happen synchronously versus asynchronously?" Strong answer: Ask whether the user needs the result of that specific piece of work immediately, within this response, to proceed — work whose result isn't needed immediately (sending a confirmation email, updating analytics) is a candidate for asynchronous processing, moved off the critical path to reduce perceived latency and isolate the user experience from that work's own failure modes.

  2. "What are the two main systems-level benefits a message queue provides?" Strong answer: Decoupling (the producer doesn't need to know how or by whom the work will be handled) and load absorption (a sudden burst of work queues up to be processed at a sustainable pace, rather than directly overwhelming the downstream consumer).

  3. "Why must consumers of an asynchronous message queue typically be designed to be idempotent?" Strong answer: Most real message queues provide at-least-once delivery, meaning a message can occasionally be delivered and processed more than once under failure conditions; a non-idempotent consumer (like one that unconditionally charges a card) would produce a harmful duplicated effect, while an idempotent one checks and ensures the same end state regardless of how many times the message is processed.

  4. "What's the difference between a simple queue and a pub/sub system, and how would you decide which fits a given asynchronous need?" Strong answer: A simple queue distributes each message to exactly one consumer from a pool; pub/sub delivers each published message to every independent subscriber — the choice depends on whether exactly one consumer should handle a given piece of work, or whether multiple independent parts of the system all need to react to the same event.

Chapter summary / cheat sheet

  • Move work off the synchronous request path when the user doesn't need its result immediately to proceed — this reduces perceived latency and isolates the user experience from that work's own failure modes.
  • Message queues provide decoupling (producer doesn't need consumer details) and load absorption (bursts queue up rather than overwhelming downstream consumers).
  • Choose a simple queue for "exactly one consumer handles this" needs; choose pub/sub for "every interested party reacts independently" needs (like Chapter 5's cache invalidation).
  • Design consumers to be idempotent by default, since most real queue systems provide at-least-once delivery — a non-idempotent consumer risks real, harmful duplicate-processing bugs under occasional redelivery.