If you remember five things
Carry these into every design round. Everything below is the defense of these five.
- Draw the line. On any write path, split the work into "must happen before I reply" and "can happen after." The "after" bucket is where queues live.
- At-least-once is the default. Real systems never lose a message but can deliver duplicates, so every consumer must be safe to run twice.
- Idempotency is the price. Attach a unique key to each message and record what you processed, so a redelivered message is a no-op instead of a second charge.
- Store durable, then publish. Write the source of truth first, enqueue second, so a message never points at data that was never saved.
- Have a failure story. Visibility timeout, redelivery, capped retries, dead-letter queue. Say it as one fluent sentence and you sound like you have run one in production.
1The mental model (what this really is)#
A queue is a buffer between the thing that creates work and the thing that does it. That is the whole idea: take a task off the critical path of a user request and hand it to something else to finish later.
The ordering-food picture, because it maps exactly.
- The cashier takes your money and hands you a number. They do not stand there and cook.
- The kitchen pulls tickets off a rail and cooks at its own pace.
- Twenty people order at once and the line still moves, because taking the order is fast.
- The queue is the ticket rail. The cashier is the producer, the kitchen is the consumer.
Three words fall out of that picture, and interviewers listen for all three.
- Decoupling. The producer does not know who does the work or how long it takes. It drops a message. You can add a second consumer, swap the implementation, or take the kitchen offline for a minute, and the cashier never changes.
- Buffering. When traffic spikes, work piles up in the queue instead of crashing the consumer. A 10x spike becomes a longer line, not a wall of 500 errors.
- Backpressure. A queue growing faster than consumers drain it is a signal. Add consumers, shed load, or slow producers. Rising queue depth is the earliest warning you get.
On any write path, the first thing I do is draw a line down the middle: does this work need to happen before I reply to the user, or can it happen after? Charging a card is before. Sending the receipt is after. The "after" bucket is the entire case for a queue, and naming that split unprompted is what reads as senior.
2Why it matters (sync versus async)#
Synchronous means the caller waits: request comes in, you do all the work, you reply. Simple, and correct for most reads. It breaks when the work is slow, flaky, or spiky.
The video-upload case, done inline, is bad on every axis.
- Transcoding takes 90 seconds. Do it inside the HTTP request and the user stares at a spinner for a minute and a half.
- Your web server thread is pinned the whole time.
- One failure means they re-upload the whole file.
- It gets worse under load: 50 uploads at once, each pinning a thread for 90 seconds, and a 200-thread web tier is saturated after four uploads. Everyone else gets refused.
Async flips it.
- The upload handler writes the file to storage, drops a
transcodemessage on a queue, and returns202 Acceptedin about 200ms. - A pool of workers picks up transcode jobs and grinds through them.
- The user polls or gets a notification when it is done.
What you bought, line by line.
- Fast responses. 90,000ms down to 200ms, and the thread returns to the pool immediately.
- Independent scaling. Web servers and workers scale on different signals. Ten web boxes, fifty workers, whatever the math says. A video goes viral, you add workers without touching the web tier.
- Resilience. A worker crashes mid-job and the message goes back on the queue for another worker. The user never knows.
- Smoothing. A marketing blast sends 100k uploads in a minute. The queue holds them, workers drain at a steady 500/sec, the backlog clears in about 200 seconds, and nobody gets a failed upload.
You traded simplicity for eventual consistency. The work is not done when you reply. You now own retries, ordering questions, duplicate handling, and a new piece of infrastructure that can itself break. Candidates who skip this line sound like they read about a queue. Candidates who say it sound like they have been paged by one at 3am.
3The template (how it works, with pseudocode)#
Every async design has the same skeleton. Learn it once, reuse it everywhere.
# Producer: on the user's critical path, do the minimum
def handle_upload(request):
file_id = store_to_blob(request.file) # durable first
queue.publish("transcode", {
"file_id": file_id,
"user_id": request.user_id,
"requested_at": now(),
"idempotency_key": file_id, # see gotchas
})
return Response(202, {"status": "processing", "id": file_id})
# Consumer: pull, do work, acknowledge
def worker_loop():
while True:
msg = queue.receive(visibility_timeout=120) # lock it for 2 min
if not msg:
continue
try:
if already_done(msg.idempotency_key): # dedupe
queue.ack(msg)
continue
transcode(msg.file_id)
mark_done(msg.idempotency_key)
queue.ack(msg) # only now remove it
except RetryableError:
queue.nack(msg) # back to queue, retry later
except FatalError:
dead_letter_queue.publish(msg) # park it, alert a human
queue.ack(msg)
The load-bearing details most people skip:
- Store durable state before you publish. Blob first, then the message. Publish first and have the store write fail, and you have a message pointing at nothing plus a worker that keeps failing to find a file that was never saved.
- Visibility timeout. On receive, the queue hides the message for N seconds instead of deleting it. Here it is 120s, comfortably past the 90s transcode. Finish and ack, the message is gone. Die mid-job, the timeout expires and the message reappears for someone else. This is how at-least-once delivery works.
- Ack only after success. Acknowledging means "I am done, delete this." Ack too early and a crash loses the work.
- Dead-letter queue (DLQ). After some number of failures (typically a cap of 3 to 5), stop retrying and move the message aside. Otherwise one poison message loops forever and burns your workers.
Set the visibility timeout too low, say 60s against a 90s job, and I will catch it: a slow-but-alive worker gets its job stolen and done twice. If you quote a timeout, tie it to the work's real duration out loud. That one sentence tells me you have actually tuned one of these, not just drawn it.
Reveal the answer
4A worked example: an e-commerce order#
Interviewer: Design the checkout flow for an online store. A user clicks Buy Now. Walk me through it.
First I split the work into must-happen-now versus can-happen-after.
Synchronous (the user waits, must be correct before I reply):
- Validate the cart and inventory.
- Charge the payment.
- Create the order record.
I keep these inline because the user needs a yes or no, and I cannot say "order placed" if the card declined. Any step fails, I reply with an error. The whole path is a few hundred milliseconds: say 50ms to validate, 300ms for the payment gateway, 20ms to write the order, so roughly 370ms end to end.
Asynchronous (fire these off, reply immediately):
- Send the confirmation email.
- Decrement warehouse stock in the fulfillment system.
- Update the recommendation model.
- Notify the analytics pipeline.
- Trigger the shipping label.
None of those block the "Order placed!" screen. Run all five inline and the user waits on the slowest one, maybe a flaky email provider that occasionally takes 4 seconds. So right after the order commits, the order service publishes one order.placed event and returns.
The decision the interviewer is waiting for: one queue with many consumers, or a topic with fan-out?
- I want five different systems to react to this one event independently. That is publish-subscribe.
- I publish
order.placedto a topic. Email, inventory, analytics, and so on each have their own subscription and their own queue. - Email being slow does not back up inventory.
- Each consumer fails and retries on its own.
# After order commits:
events.publish(topic="order.placed", {
"order_id": order.id,
"user_id": user.id,
"items": order.items,
"total": order.total,
"event_id": uuid(), # for idempotency downstream
})
Then I say the sentence that scores points:
Candidate: The email service might get this event twice because I am using at-least-once delivery, so sending a confirmation email has to be idempotent. I dedupe on
event_idso the customer never gets two receipts for one order.
That one line shows you understand the guarantee you chose and its consequence. That is the gap between drawing a queue and designing with one.
Weak answer versus strong answer#
Interviewers grade this exact moment. Here is the contrast worth drilling.
Weak answer
- Topology
- One queue, one worker, everything bundled.
- Guarantee
- Never named.
- Duplicates
- No answer.
- Breaks when
- "Email provider is down for 10 minutes." Inventory and analytics back up behind the dead email service, and there is nothing to point at.
Strong answer
- Topology
- One event to a topic with five independent subscribers, each with its own queue.
- Guarantee
- At-least-once, stated.
- Duplicates
- Each consumer dedupes on event_id.
- Breaks when
- It does not: a slow email provider backs up only the email queue, and after 5 failures that one message goes to a DLQ and alerts while everything else flows.
Same starting diagram. The strong version names the fan-out topology, the guarantee, the dedupe key, the failure isolation, and the DLQ. That is four or five extra sentences, and it is the whole difference between "mid" and "pass."
5Variations and gotchas#
Delivery guarantees. Three of them, and you should know which you are picking and why.
- At-most-once. Fire and forget, might lose messages. Fine for metrics or best-effort logs where losing 0.1% does not matter.
- At-least-once. The default for most systems. Never loses a message, can deliver duplicates. You handle dupes with idempotency.
- Exactly-once. The holy grail, and mostly a marketing term across network boundaries. What systems like Kafka actually give you is exactly-once processing within their own boundary. Across services, "exactly-once" in practice means "at-least-once delivery plus idempotent consumers." Say it that way and you sound like you have been burned before.
Idempotency. The single most important word in async design.
- You will get duplicates, so every consumer must be safe to run twice.
- Attach a unique key to each message and record what you have processed.
- "Charge $50" is dangerous to replay. "Set order 123 to paid" is safe.
- Design operations to be the second kind whenever you can.
Ordering. A plain queue does not guarantee global order, and getting it often costs parallelism.
- The usual answer is partition by key: all events for
user_123go to the same partition and stay ordered relative to each other, while different users process in parallel. - You rarely need total ordering. You often need per-entity ordering.
- Know the difference, because an interviewer who hears "I need everything in order" will immediately ask what that does to your throughput.
Poison messages. One malformed message that always throws will retry forever and clog the works.
- The DLQ exists for exactly this.
- Cap retries at 3 to 5, then park it and alert.
Queue backup. Consumers die, producers keep going, the queue grows without bound.
- You need a plan: alert on queue depth and consumer lag, autoscale consumers off those metrics, shed load in the worst case.
- Concretely: producers push 1,000 msg/sec, workers drain 800/sec, backlog grows by 200 every second, which is 720,000 messages an hour.
- That is a page, not a dashboard you check tomorrow.
6Trade-offs (the real ones)#
| Choice | You gain | You pay |
|---|---|---|
| Async over sync | Fast responses, resilience, independent scaling | Eventual consistency, duplicate handling, more moving parts |
| At-least-once | No lost messages | Must build idempotency |
| Pub-sub fan-out | Consumers evolve independently | More queues to operate and monitor |
| Strict ordering | Correct sequencing | Lost parallelism, lower throughput |
| Bigger visibility timeout | Fewer premature redeliveries | Slower recovery when a worker truly dies |
The meta-trade-off: a queue adds a dependency.
- It can be down, slow, or full.
- Do not add one for work that is genuinely fast and must be consistent.
- Charging a card through a queue so the user does not know if they paid is a bad design.
- Use async for work that can tolerate being finished a little later.
Rough numbers to anchor you.
- An in-request slow task might be 500ms to several seconds. A queue publish is sub-millisecond to a few ms.
- A managed queue like SQS handles effectively unbounded throughput.
- Kafka pushes hundreds of thousands to millions of messages per second per cluster.
- RabbitMQ sits in the tens of thousands per second range on a single node.
- A healthy consumer-lag target is seconds, not minutes. Lag climbing steadily means you are under-provisioned, full stop.
A queue is for in-flight work, not your source of truth. Durable state lives in a store; the queue only coordinates who processes it.
7Mistakes that cost people the round#
- Drawing a queue with no reason. "And then a queue here." Why? If you cannot name the slow or spiky work it absorbs, delete it.
- Never mentioning the delivery guarantee. Silence reads as "I memorized a diagram." Say "at-least-once, so consumers are idempotent" out loud.
- Forgetting idempotency. The classic follow-up is "what if that message is delivered twice?" No answer means you look junior.
- No failure story. "What if a worker crashes mid-job?" Visibility timeout, redelivery, retries, DLQ. Have it ready as one fluent sentence.
- Confusing a queue with a database. A queue holds in-flight work, not your source of truth.
- Claiming exactly-once with a straight face. Reach for it and a good interviewer pokes the hole. Say "at-least-once plus idempotent" and you close the hole yourself.
8Your practice path#
Build the reflex in this order:
- Email / notification sender. The simplest async job. Practice producer, worker, retry, idempotent send.
- Image or video processing pipeline. Adds the store-first pattern and long-running jobs with visibility timeouts.
- E-commerce order flow (section 4). Forces the sync-versus-async split and pub-sub fan-out.
- Rate limiter / job scheduler. Introduces delayed messages and scheduled work.
- Analytics or event ingestion at scale. Now partitioning, per-key ordering, and consumer lag matter. This is where the big-system vocabulary shows up.
For each one, say out loud: what is sync, what is async, what guarantee, how do I dedupe, what happens when a consumer dies. Answer those five for any design and you are ready.
9Where this shows up#
Async processing appears in nearly every system design round above entry level.
- URL shortener: analytics on clicks goes async.
- Uber: driver location fan-out and ride-event processing.
- Payment system: webhooks, reconciliation, and retries are all queue-shaped.
- Social feed: fan-out on write is a queue problem.
- Notifications, ride-sharing, e-commerce, and streaming lean on it hardest, and shops like Amazon, Uber, and any payments or logistics company expect you to reach for it naturally.
"What if that message gets delivered twice?"
"The email provider is down for ten minutes. What happens to the rest of the order flow?"
"A worker crashes mid-job. Do you lose the work?"
"I need these events processed in order. Can your queue do that?"
"Can you just give me exactly-once delivery?"
What you can now explain in the room
- Where to draw the sync-versus-async line on any write path, and why charging a card stays inline.
- Why at-least-once is the real default and how idempotency pays for it.
- How visibility timeout, ack-after-success, and the DLQ combine into a clean crash story.
- When to pick pub-sub fan-out over a single queue, and what failure isolation you get for it.
- Why per-entity ordering (partition by key) beats asking for global order.
- Why "exactly-once" across services is really at-least-once plus idempotent consumers.
Head over to the company guides to see exactly which rounds and questions put message queues front and center.
Get a prep plan built around your Message Queues and Async Processing interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.