System Design Interview Questions: Message Queues
Reviewed by Mark Dickie · Last updated
Message queues are asynchronous communication primitives that decouple producers from consumers by holding messages in a buffer until they can be processed. In a system design interview, message-queue questions test whether you can choose the right queuing model for a throughput or reliability requirement, explain delivery semantics, and reason about ordering, back-pressure, and failure handling under load. You should be ready to compare point-to-point vs. publish-subscribe topologies, justify at-least-once vs. exactly-once delivery, and describe how dead-letter queues and idempotent consumers keep a pipeline from silently dropping data when downstream services fail. Interviewers also expect you to tie queue choices back to concrete tradeoffs in latency, durability, and operational complexity rather than naming technologies at random. The quiz below covers these areas with real interview questions drawn from system design rounds. When practicing, focus on articulating the tradeoff in plain terms, not just the pattern name.
What does a message-queue interview question test?
A typical prompt gives you a high-level system (a ride-sharing app, a payment pipeline, a notification fan-out) and asks how you would move work between components. The interviewer is probing several things at once: whether you recognize when synchronous calls will bottleneck the system, whether you can pick a queue type that matches the access pattern, and whether you can explain what happens when a consumer crashes mid-processing. They want to see you reason about ordering guarantees, replay, and poison messages, not just draw a box labeled 'Kafka.'
Core message-queue concepts you should know before the interview
| Concept | What to be able to explain |
|---|---|
| Point-to-point vs. pub-sub | When each model fits; how fan-out changes consumer scaling |
| Delivery semantics | The practical difference between at-most-once, at-least-once, and exactly-once, and why exactly-once is rarely free |
| Ordering | How partitioning affects per-key ordering, and what you give up for parallelism |
| Back-pressure | How a queue absorbs load spikes and what happens when the buffer fills |
| Dead-letter queues | Where failed messages go, how you replay them, and why idempotency matters on retry |
| Durability & replication | What persisted-to-disk vs. replicated means for recovery and latency |
| At-least-once + idempotency | Why the common production pattern pairs at-least-once delivery with idempotent consumers instead of chasing exactly-once |
How to structure your answer to a message-queue design question
- Identify the producer-consumer boundary — name what sends, what receives, and whether multiple consumers compete or each get a copy.
- State the durability requirement — can you tolerate message loss on crash, or must messages survive a node failure?
- Pick a delivery semantic and justify it — at-least-once with idempotent consumers is the safe default; explain why exactly-once adds cost.
- Address ordering — decide whether global order, per-key order, or no ordering guarantee is needed, and how that choice limits parallelism.
- Plan for failure — describe dead-letter handling, replay, monitoring of queue depth, and what alerts you would set.
- Quantify when possible — give rough throughput numbers, partition counts, or retention windows so the interviewer sees you can size the system, not just name the parts.
Common message-queue interview follow-ups
Interviewers often push past your initial design with pointed follow-ups. They may ask what happens if a consumer takes too long and the visibility timeout expires (another consumer picks up the message, causing duplicate processing). They might ask how you handle a sudden 10x traffic spike (the queue absorbs it, but consumer autoscaling and queue-depth monitoring need to be in place). They could ask whether you would use Kafka or RabbitMQ for a specific workload and expect you to compare partition-based log durability against traditional broker semantics rather than just naming the tool.
Key facts
- Tarmac's System Design interview questions cover 20 questions at difficulty 3–5 of 5.
- Tarmac tracked 4,937 job postings asking for System Design in August 2026.
- Roles asking for System Design advertise a median base salary of US$181,600, across 1,164 job postings as of August 2026.
- Tarmac last reviewed these System Design interview questions on 31 August 2026.
At a glance
| Questions | 20 |
|---|---|
| Difficulty | 3–5 of 5 |
| Formats | Multiple choice, Multiple answer, Short answer, Flashcard, True / false, Ordering, Find the bug, Design exercise |
What you'll review
- consumer groups
- dead letter queue
- message queues
- delivery semantics
- caching strategies
Practice questions
Try one before you open the answer. Pick an option and press Check; it's marked on the spot.
System Design/sd-patterns/consumer-groups
A Kafka topic has 4 partitions. One consumer group reads it, and you scale the group from 4 to 8 instances to double throughput — but throughput stays flat and 4 instances sit completely idle. Why?#
Options
Show answer
Within a consumer group each partition is assigned to at most one consumer, so parallelism is capped at the partition count — here 4 — and the extra 4 instances get no partition and sit idle as standbys. The unit of parallelism is the partition, not the consumer. To raise consumer throughput you must add partitions, not consumers. Idle standbys are still useful for fast failover.
Inside a consumer group the unit of parallelism is the partition: each partition is owned by exactly one consumer in the group at a time. With 4 partitions, at most 4 consumers do work; the other 4 are idle standbys (useful only for fast failover if an active consumer dies). To raise consumer parallelism you must add partitions, not consumers. The distractors invert this: consumers in a group do not share a partition round-robin (a), there is no fixed per-group instance cap (b), and consumers absolutely can be the bottleneck (d) — that is the whole reason to scale them, up to the partition ceiling. Separate consumer groups, by contrast, each receive their own full copy of the stream (pub-sub fan-out).
System Design/sd-patterns/dead-letter-queue
A message consumer crashes with a deserialization error every time it processes a particular message, causing the broker to keep redelivering it — consuming CPU indefinitely. Which mechanism is specifically designed to break this loop?#
Options
Show answer
A dead-letter queue (DLQ) breaks the loop: after a configured number of delivery attempts the broker routes the failing message to the DLQ for inspection, freeing the main queue to keep processing healthy messages. Engineers can then diagnose the bad payload at their own pace. Switching to at-most-once or a short TTL silently drops the message, and adding consumers just spreads the same deserialization crash to more nodes.
A dead-letter queue is the correct tool: after a configurable retry ceiling (e.g. 5 delivery attempts) the broker automatically moves the problematic message to a separate DLQ so it can be inspected, replayed, or discarded without blocking the main queue. The consumer keeps processing the rest of the stream while engineers diagnose the bad payload at their own pace. Switching to at-most-once (b) silently drops messages on failure — you lose the event permanently without knowing why it failed. A short TTL (c) is a blunt forced expiry that also loses the message with no diagnostic value. More consumers (d) just propagate the crash to more instances — every consumer hits the same deserialization error on the same payload, amplifying the problem rather than containing it.
System Design/sd-patterns/message-queues
Your checkout flow synchronously calls an email service, an analytics pipeline, and a fraud-scoring job, and a slow dependency now blocks orders. You introduce a message queue so checkout publishes events and workers consume them. Which benefits does this asynchronous decoupling genuinely provide?#
Options
Pick every one that applies.
Show answer
The real benefits are that checkout no longer blocks on slow consumers (it returns once the event is enqueued), the queue absorbs traffic spikes and buffers work so consumers drain at their own pace, and a temporarily down consumer can recover and process the backlog without losing events. It does not guarantee lower per-request end-to-end latency, and it does not remove the need to handle duplicate deliveries.
A queue decouples producers from consumers: checkout returns as soon as the event is enqueued (a), the queue acts as a buffer that smooths spikes so consumers process at a sustainable rate (b), and durable queues retain messages so a consumer that was down can drain the backlog on recovery (c). The two wrong options reflect common misconceptions. Async processing does not lower per-request end-to-end latency (d) — the downstream work still happens, just later; you trade latency-to-completion for responsiveness and resilience. And most queues offer at-least-once delivery, so consumers must be idempotent to tolerate duplicates (e); the queue does not remove that obligation.
System Design/sd-patterns/message-queues
When would you add a message queue between two services, and what does it buy you?#
Show answer
You add a message queue when the producer should not be coupled to the consumer's availability or speed — for example offloading slow work (sending email, generating a report, running an inference job) from the request path. The queue decouples the two services so the producer can keep accepting work even if the consumer is down, it absorbs traffic spikes by buffering, and it smooths load so a slow consumer can drain the backlog asynchronously. It also enables retries and fan-out, at the cost of eventual consistency and harder end-to-end tracing.
A message queue introduces asynchrony and decouples producer from consumer: the producer enqueues and returns immediately, while the consumer processes at its own pace. This buys availability (the producer survives a consumer outage), load smoothing and buffering against spikes, retry/dead-letter semantics, and fan-out to multiple consumers. The trade-offs are eventual consistency, delivery-semantics complexity (at-least-once usually, so consumers must be idempotent), and reduced observability across the async boundary.
System Design/sd-patterns/message-queues
What is a dead-letter queue (DLQ), what problem does it solve with a 'poison' message, and why is a DLQ better than simply retrying forever?#
Show answer
A dead-letter queue is a separate queue where messages are routed after they fail processing a configured number of times. It solves the poison-message problem: a single malformed or un-processable message that always throws. Without a DLQ, an at-least-once queue keeps redelivering that message, the consumer keeps failing on it, and it blocks (head-of-line) or endlessly burns the consumer's capacity — and in an ordered partition it can stall everything behind it. Sending it to the DLQ after N attempts gets the bad message out of the main flow so healthy messages keep moving, while preserving the failed message (with its error context) so an operator can inspect, fix, and replay it instead of silently dropping it. So a DLQ contains the blast radius and makes failures observable, where retry-forever just amplifies the outage.
A dead-letter queue is the sidelining destination for messages that fail processing past a retry threshold. It exists to contain the poison message — one that always fails — which under at-least-once delivery would otherwise be redelivered forever, burning consumer capacity and, in an ordered partition, blocking everything behind it (head-of-line blocking). Routing it to the DLQ after N attempts keeps the main pipeline flowing while preserving the failed message and its error context for an operator to inspect, fix, and replay, rather than dropping it silently. Retry-forever just amplifies a localized failure into a stalled pipeline; the DLQ bounds the blast radius and makes the failure observable.
System Design/sd-patterns/message-queues
What benefits does putting a message queue between a producer and a slow consumer provide?#
Show answer
A queue decouples producer and consumer in time and rate. It absorbs traffic spikes by buffering (load levelling / backpressure), so a burst doesn't overwhelm a slow downstream; it lets consumers scale independently and process at their own pace; it improves resilience because a consumer outage just lets the backlog grow instead of dropping work; and it enables async, fire-and-forget workflows so the producer returns quickly. The cost is added latency, at-least-once delivery semantics (requiring idempotent consumers), and operational complexity.
Queues turn a synchronous, tightly-coupled call into an asynchronous pipeline, which is how you keep a user-facing request fast while heavy work (emails, video transcoding, billing) happens out of band. The key correctness caveat is delivery semantics: most queues guarantee at-least-once, so consumers must dedupe or be idempotent.
System Design/sd-patterns/message-queues
A service must update its database and publish an event to a message broker in the same logical operation, but if the broker publish fails after the DB commit the event is silently lost. Which pattern reliably guarantees both are eventually durable without a distributed transaction?#
Options
Show answer
Use the transactional outbox: write the event to an outbox table in the same local DB transaction as the business change, then have a relay process read and publish pending outbox rows. The business update and the outbox row both commit or both roll back, and relay-side retries make the publish at-least-once — no distributed transaction needed. Dual-write or write-broker-first both leave a gap where one side can be lost.
The transactional outbox solves the dual-write problem by using the DB's own atomicity: the business update and the outbox row land in one local transaction, so they either both commit or both roll back. A separate relay (or change-data-capture feed) publishes outbox rows to the broker and marks them delivered; retries on the relay side make the publish at-least-once. No distributed lock or 2PC is needed because the source of truth for 'did the event need to be sent' is the DB itself. Inline retry on dual-write (b) blocks the caller and still loses events if the process dies between the DB commit and a successful broker publish. Write-broker-first (c) reverses the dependency but introduces the same gap on the other side: the broker message is live but the DB write might never happen. Using a saga compensating transaction (d) adds complexity and still cannot guarantee the broker publish eventually succeeds — it would undo the real business change, which is the wrong behavior if the intent was to commit both.
System Design/sd-patterns/message-queues
A producer publishes messages 10x faster than the consumer can process them. The queue depth grows unboundedly and eventually the broker runs out of memory and crashes. Which architectural response most directly addresses the root cause?#
Options
Show answer
Apply backpressure: have the broker or consumer signal the producer to slow down (or have the producer check queue depth before publishing) so ingest rate is bounded by consumer capacity. The root cause is a sustained producer-consumer rate mismatch, and backpressure closes the feedback loop. Raising the queue limit only delays the OOM, a CDN is the wrong layer, and an in-memory broker doesn't change consumer throughput.
The root cause is a sustained producer-consumer rate mismatch: more flows in than flows out, so the queue grows without bound. Backpressure closes the feedback loop — the producer is told (or infers) that it needs to slow down, throttle, or shed load to match what the downstream can handle. This is the canonical fix. Raising the queue limit (b) just delays the inevitable — the rate gap is still there, and you now need a larger machine to OOM. A CDN (c) absorbs bursty read traffic, not producer message bursts; it is the wrong layer. Switching to in-memory (d) trades persistence for speed on the broker side but doesn't change consumer processing capacity — the consumer still processes at the same rate, so the queue still grows, now in RAM with no persistence if the broker crashes.
System Design/sd-patterns/delivery-semantics
Your team is debating delivery semantics for a Kafka-based event pipeline that charges customer accounts. Which statements about delivery guarantees are correct?#
Options
Pick every one that applies.
Show answer
The correct statements are that at-least-once delivery means the consumer may see a message more than once and must be idempotent, that exactly-once is achievable in Kafka with idempotent producers and transactions but adds latency and throughput overhead, and that consumer-side idempotency (storing a processed event id) yields an effectively-exactly-once outcome under at-least-once delivery. At-most-once is wrong for billing — it drops charges — and committing the offset before processing gives at-most-once, not at-least-once.
Understanding delivery guarantees is essential for financial pipelines. At-least-once (a) is the practical default: the broker re-delivers on timeout, so consumers see duplicates and must deduplicate or be idempotent. Exactly-once (b) is achievable in Kafka using idempotent producers and the Kafka transactions API, but the coordination adds measurable latency — it's a deliberate trade-off. Consumer-side idempotency with a processed-ID store (d) is the practical alternative: even under at-least-once delivery, recording each processed event key and skipping already-seen keys gives the observable outcome of exactly-once without the Kafka transaction complexity. At-most-once (c) is exactly wrong for a billing pipeline: it permits dropped messages, meaning some charges are silently never applied — the acceptable side of the error budget is duplicate detection, not missed work. Committing the offset before processing (e) gives at-most-once (if the consumer crashes after commit but before processing, the message is lost), not at-least-once; at-least-once requires committing after successful processing.
System Design/sd-patterns/message-queues
A message queue configured for at-least-once delivery can safely be consumed by a non-idempotent handler without risk of data corruption, provided the consumer acknowledges quickly.#
Options
Show answer
False. At-least-once delivery guarantees every message arrives at least once but explicitly permits duplicates — a broker re-queues a message whenever an acknowledgement is lost or delayed, no matter how fast the consumer acks. A non-idempotent handler will corrupt data on duplicate delivery. The only safe pairing is an idempotent consumer that detects and ignores redeliveries via a dedup key or upsert.
At-least-once delivery guarantees every message is delivered at least once but explicitly permits duplicates — a broker re-queues a message whenever an acknowledgement is lost or delayed, regardless of how fast the consumer acks. A non-idempotent handler (e.g. one that deducts a balance or inserts a row without a unique constraint) will corrupt data on duplicate delivery. The only safe pairing is an idempotent consumer — one that detects and safely ignores redeliveries, typically via a deduplication key or an upsert.
System Design/sd-patterns/message-queues
Adding more consumers to a Kafka consumer group always increases throughput, regardless of the number of partitions on the topic.#
Options
Show answer
False. Kafka assigns at most one consumer per partition within a consumer group, so with 4 partitions and 6 consumers only 4 receive messages while the other 2 sit idle. True parallelism is bounded by the partition count, not the consumer count. To increase throughput beyond it you must add partitions — a one-way, non-trivial change that affects key ordering.
Kafka assigns at most one consumer per partition within a consumer group. If you have 4 partitions and 6 consumers, only 4 consumers receive messages; the remaining 2 sit idle. True parallelism is bounded by the partition count, not the consumer count. To increase throughput beyond the current partition count, you must increase the number of partitions — which is a one-way, non-trivial operation that affects key ordering guarantees.
System Design/sd-patterns/message-queues
Most production message queues deliver at-least-once, meaning a consumer can receive the same message more than once. Why does this duplication happen, and what does the consumer have to do so that reprocessing a message is safe?#
Show answer
At-least-once delivery happens because the broker only removes a message after the consumer acknowledges it; if the consumer processes the message but crashes (or the network drops) before the ack reaches the broker, the broker assumes failure and redelivers the message, so the same work runs twice. Exactly-once is hard and usually impractical end-to-end, so the standard answer is to make the consumer idempotent: design processing so that handling the same message twice has the same effect as handling it once. In practice that means deduplicating on a stable message id (record processed ids and skip duplicates), or making the side effect itself idempotent — an upsert keyed by the message's id rather than a blind insert, or a set rather than an increment — so a redelivery is a no-op.
Duplication is inherent to at-least-once: the broker waits for an ack before deleting a message, so a consumer that finishes the work but dies before acking gets the message redelivered — the work runs again. Since true end-to-end exactly-once is generally impractical, the consumer must be idempotent: processing the same message twice equals processing it once. The two standard techniques are dedup on a stable message id (persist processed ids, skip repeats) and making the side effect naturally idempotent (an upsert keyed by the id instead of an insert; set-membership instead of an increment). This is the consumer-side complement to producer-side idempotency keys.
System Design/sd-patterns/message-queues
Order the steps in a durable message delivery flow using a broker (e.g. Kafka or RabbitMQ), from producer send to consumer acknowledgement.#
Put these in order
Show answer
A durable broker delivery flow runs in this order:
- Producer calls send with the message payload and a target topic/queue.
- Broker persists the message to disk and replicates it to in-sync followers.
- Broker returns a send acknowledgement to the producer.
- Consumer polls or receives a push notification and fetches the message.
- Consumer processes the message and sends an ack (or commits the offset) to the broker.
- Broker marks the message as delivered and advances the consumer's position.
Durability requires the broker to persist and replicate before acking the producer (step 3), so a broker crash after the ack cannot lose the message. The consumer ack (step 5) is intentionally separate from receipt: it signals successful processing, not just delivery, which is what enables safe at-least-once retry. The broker only advances the consumer's committed position after that ack (step 6), ensuring unprocessed messages are re-delivered on consumer failure.
System Design/sd-patterns/dead-letter-queue
This worker pulls messages off a queue and processes them. A single malformed message has brought the whole consumer to a halt — no other message gets processed. What is the bug?#
// queue.receive() blocks until a message is available
while (true) {
const msg = queue.receive();
try {
process(msg); // throws on a malformed message
queue.ack(msg);
} catch (err) {
queue.requeue(msg); // put it back so we don't lose it
}
}Options
Show answer
A message that always throws (a 'poison pill') is requeued endlessly with no attempt limit, so it loops forever at the head of the queue and starves every other message; it needs a redelivery counter that routes to a dead-letter queue after N attempts
The message is a poison pill: it deterministically throws, the catch block requeues it with no attempt limit, and (in a FIFO queue) it returns to the head and is retried forever — blocking every message behind it. The fix is to bound redeliveries (track an attempt count or use the broker's max-receive setting) and route the message to a dead-letter queue after the threshold, so the poison message is quarantined for inspection while the rest of the stream flows. The distractors are wrong: ack-after-process (b) is the correct at-least-once ordering, not the bug — acking first would silently lose messages on a crash; the infinite while loop (c) is the intended consume loop, not a memory leak; and changing requeue to ack on failure (d) would discard every failed message, replacing a stall with silent data loss.
System Design/sd-patterns/message-queues
Design a multi-channel notification system: a platform service that other services call to send notifications to users over push, email, SMS, and in-app.#
Show answer
Requirements. A single send API fans to four channels, each fronted by a third-party provider with its own rate limit and failure profile. Two priority tiers: transactional (OTP, < 5 s) and marketing (batchable). First-class requirements beyond delivery: idempotency (never double-send), preferences/opt-out, retry, and an audit trail.
Pipeline. The send API does almost nothing synchronously — it validates and enqueues the event onto a message queue, then returns. Per-channel workers consume from their queue and call the provider. This decoupling is the backbone: it absorbs the 30K/sec peaks, and a slow provider only backs up its queue, not the others. Templating (render the message body from a template + data) happens in the worker just before send.
Dedup & idempotency. Each event carries an idempotency key (event_id:user_id:channel). Before sending, the worker does an atomic check-and-set against an idempotency store; if the key already exists, it's a duplicate and is dropped. This turns the queue's at-least-once delivery into at-most-once per recipient per channel, so an upstream retry can't double-send.
Reliability. Transient provider errors are retried with exponential backoff + jitter. A message that exhausts its retries (or is malformed/poison) goes to a dead-letter queue for inspection rather than blocking the worker. Because each channel has its own queue and worker pool, a failing SMS provider can't stall push or email.
Preferences. Before dispatch, the worker checks the user's preferences/opt-out for that channel and any per-user frequency cap (so we don't spam someone with ten pushes in a minute), plus compliance rules (quiet hours, unsubscribe for email). A suppressed notification is recorded as suppressed in the audit, not sent.
Scaling & provider limits. Each channel scales its worker pool independently. Workers pace themselves with a token bucket matched to the provider's rate limit, applying backpressure to the queue rather than overrunning the provider. Priority queues (a separate high-priority lane per channel) let an OTP jump ahead of a 50M-message marketing backlog so it still meets its < 5 s budget.
A notification system is fundamentally an asynchronous, queue-backed pipeline problem. The defining move is decoupling ingestion from delivery: the send API just enqueues, and per-channel workers deliver — which is what absorbs traffic spikes and stops one slow third-party provider from stalling the others. The two correctness pillars are idempotency (an idempotency key like event:user:channel checked before send turns the queue's at-least-once delivery into at-most-once per recipient, so upstream retries never double-send) and reliability (backoff retries plus a dead-letter queue for poison messages). Preferences/opt-out and per-user frequency caps must be enforced before dispatch, and priority queues are what let a latency-critical OTP overtake a bulk marketing backlog.
System Design/sd-patterns/message-queues
Design a distributed web crawler that fetches a large slice of the web for indexing.#
Show answer
Requirements. Crawl ~1B pages in a month while being a good citizen: obey robots.txt, never overload a host, skip already-seen URLs, and re-crawl by freshness. Two dedup problems exist — exact URL dedup (don't fetch the same link twice) and near-duplicate content (mirrors/boilerplate shouldn't be stored repeatedly). Crawler traps (infinite URL spaces) are a known hazard.
URL frontier & politeness. The heart of the design. The frontier is a set of queues feeding the fetchers, with two axes: priority (important/fresh pages first) and politeness. For politeness, URLs are routed to per-host queues, and a host's queue is drained at a capped rate (with a delay between requests) — so even if a million links point at one domain, we crawl it steadily without flooding it. Expansion is roughly BFS from the seeds.
Dedup. Tracking billions of seen URLs in an exact hash set costs too much memory, so I use a bloom filter (or a hashed, sharded seen-set): probabilistic membership at a fraction of the memory, accepting a small false-positive rate (we occasionally skip a genuinely new URL — acceptable). For content dedup, I compute a fingerprint/simhash of each page and skip storing pages whose fingerprint is near an existing one, catching mirrors and trivially-different pages.
Fetch → parse → store pipeline. Distributed fetcher workers pull URLs from the frontier. Each does DNS resolution (heavily cached — DNS is a hidden bottleneck), fetches the page, parses it to extract links, runs new links through the seen-filter and enqueues the unseen ones back into the frontier, and writes the content to storage. The stages are decoupled (queues between them) so a slow remote fetch doesn't stall parsing or storage.
Storage & scaling. ~100 TB of raw content goes to an object store, partitioned by domain hash; crawl metadata (last-crawled timestamp, content fingerprint, status) lives in a separate store keyed by URL/host. Fetcher workers scale horizontally, and the frontier is partitioned across machines (typically by host, which conveniently also enforces politeness — all of one host's URLs sit on one partition).
Robustness & traps. Infinite URL spaces (a calendar's endless 'next month') are bounded with depth limits and per-host URL-count caps plus pattern detection. Slow/malicious hosts get timeouts and are de-prioritised; failed fetches are retried a few times then skipped. The frontier and seen-set are persisted/checkpointed so a worker crash doesn't lose progress or re-crawl everything.
A web crawler's design is dominated by two problems most candidates underweight. First, the URL frontier with politeness: because the web is densely interlinked, a naive queue would hammer popular domains, so URLs are routed into per-host queues drained at a capped rate — you crawl a domain thoroughly without flooding it (partitioning the frontier by host conveniently enforces this). Second, dedup at billions scale: an exact seen-URL hash set is too large for memory, so a bloom filter gives cheap probabilistic membership (trading a rare false-positive skip for huge memory savings), while simhash/fingerprint content dedup catches mirrors. The supporting pipeline decouples fetch/parse/store, caches DNS (a hidden bottleneck), and bounds crawler traps with depth and per-host URL caps so an infinite calendar can't trap a worker forever.
System Design/sd-patterns/message-queues
Design a distributed task / job queue (think Celery / Sidekiq / SQS-backed workers): producers submit background jobs, a fleet of workers executes them, and the system must not lose work or run a job twice in a way that corrupts state.#
Show answer
Requirements. The system has two halves: submit (cheap, synchronous — durably record the job and return) and execute (asynchronous, on a worker). Jobs vary wildly in duration (10 ms to minutes) and need scheduling and priority. The crucial semantic is delivery: I'll deliver at-least-once and make handlers idempotent, which gives exactly-once effect even for charge-card — true exactly-once delivery across a crash boundary is not achievable, so I don't promise it.
Queue & leasing. Jobs go into a durable, replicated broker (Kafka, SQS, or a Redis stream). A worker doesn't delete on dequeue — it leases the job with a visibility timeout (or in-flight/ack semantics): the job is hidden from other workers while being processed and is only acked (removed) on success. If the worker crashes, the lease expires, the job reappears, and another worker runs it. This is what guarantees no lost jobs on crash, at the cost of possible re-delivery.
Delivery semantics & idempotency. Because re-delivery is possible, every side-effecting handler is idempotent, keyed by a job/idempotency id. For charge-card: before charging, the worker does an atomic check-and-set on idempotency_key in a store; if it's already marked done, it skips the charge and just re-acks. So the second worker that picks up the re-delivered job sees the key, does nothing, and acks — the card is charged exactly once even though the job was delivered twice.
Retry, DLQ, scheduling, priority. Transient failures retry with exponential backoff + jitter (bounded attempts). A job that exhausts retries — or is malformed/poison — is moved to a dead-letter queue for inspection rather than looping forever. Scheduled jobs go to a delay queue or a timestamp-indexed store that a promoter moves into the ready queue when due. Priority is separate lanes (high/default/low) or a priority field, so urgent jobs jump ahead.
Scaling the queue & fleet. The broker is partitioned (by job type or key hash); throughput scales with partition count and worker count. The fleet autoscales on queue depth / consumer lag. Critically, long-running jobs get their own queues and worker pools, isolated from latency-sensitive ones — so a flood of 5-minute transcodes drains its own pool and never sits in front of send-email.
Failure modes. Broker down: it's a replicated durable log, so it tolerates node loss; producers buffer briefly. Worker crash mid-job: lease expiry → redelivery (idempotency makes that safe). Poison job: capped at N attempts then DLQ, so it can't crash workers forever. Head-of-line blocking: per-type queues plus priority lanes keep one backlog from starving the rest.
A distributed job queue is the canonical at-least-once + idempotency system. The defining move is leasing instead of delete-on-dequeue: a worker hides a job with a visibility timeout and only acks on success, so a crash mid-job makes the job reappear and re-run rather than vanish — which is exactly why no work is lost. The price of that guarantee is possible re-delivery, so every side-effecting handler must be idempotent (an idempotency key checked before the side effect), which is what turns at-least-once delivery into exactly-once effect even for a charge-card. The supporting machinery is backoff retries with a dead-letter queue for poison jobs (so one malformed job can't loop forever), scheduled/priority lanes, and — the most-missed point — isolating long-running jobs in their own pools so a transcode backlog never blocks fast jobs (head-of-line blocking).
System Design/sd-patterns/message-queues
Design the event-driven backbone for an online order pipeline. When a customer places an order, several independent services must react — inventory, payments, fulfilment/shipping, notifications, analytics — and the company wants them decoupled so adding a new reaction (e.g. a loyalty-points service) doesn't mean changing the checkout code.#
Show answer
Requirements. The goal is decoupling: checkout publishes an OrderPlaced event and doesn't know or care who consumes it, so adding a loyalty service later is a pure consumer change. Ordering matters within an order (reserve → charge → ship) but not across orders. And it must be durable (no order event lost) with idempotent consumers because redelivery will happen.
Backbone & topology. I use a durable, partitioned log (Kafka-style), not a fire-and-forget bus. Producers publish to a topic per event type (orders). Each reacting service is its own consumer group with its own committed offset, so every service independently reads the whole stream. Adding the loyalty service means standing up a new consumer group subscribed to orders — the producer doesn't change at all. That's the decoupling payoff.
Ordering & partitioning. I partition by order_id. All events for one order hash to the same partition and are therefore consumed in order (reserve before charge before ship). Different orders hash to different partitions, so thousands of orders are processed in parallel across partitions. The partition key is the single knob that buys per-order ordering and cross-order throughput at the same time — global ordering would serialise everything and kill throughput, which we don't need.
No-loss delivery & idempotency. The log is replicated and durable, and a consumer commits its offset only after it has successfully processed a message — so a crash mid-processing means the message is re-read (at-least-once), never skipped. Because re-processing can happen, each consumer is idempotent: it dedups by (event_id / order_id, step) against a processed-set or makes the side effect naturally idempotent, so a redelivered charge doesn't bill twice.
Independent consumer pacing. Per-group offsets plus the log's retention mean consumers run at wildly different speeds without interfering: analytics flies ahead, the slow shipping consumer lags but its backlog just sits buffered in the log — it never slows the producer or the other consumers. To speed a slow consumer up, add partitions and consumer instances within its group so its work parallelises.
Poison events & failure. A message that crashes the shipping handler every time would, if retried in place, block its partition forever (head-of-line blocking) — every later order for that partition stuck behind it. So after bounded retries with backoff, I route the poison event to a dead-letter topic for inspection and move the offset on, freeing the partition. A fully-down consumer is fine: the log retains events, so on recovery it resumes from its last committed offset and catches up — nothing is lost.
An event-driven order pipeline is the canonical pub/sub decoupling + ordered partitioning design. The decoupling comes from a durable partitioned log (Kafka-style) with a consumer group per service: each service reads the whole stream at its own offset, so a new reactor (loyalty points) is added by subscribing — the producer never changes. The defining technical move is partitioning by order_id, which delivers per-order ordering (all of one order's events on one partition, consumed in sequence) while letting different orders run in parallel across partitions — one key buying both ordering and throughput, where global ordering would have killed it. No-loss comes from a replicated log plus committing offsets only after successful processing (at-least-once), which forces consumers to be idempotent against redelivery. The most-missed failure mode is the poison event causing head-of-line blocking — a message that crashes a consumer every time will freeze its whole partition, so after bounded retries it must go to a dead-letter topic; and because the log retains events, a fully-down consumer simply catches up on recovery.
System Design/sd-patterns/message-queues
Design a real-time chat system (think WhatsApp / Messenger) supporting 1:1 and group conversations, with online/offline delivery and read receipts.#
Show answer
Requirements. Two delivery modes dominate: online (push immediately over a live connection) and offline (store and deliver on reconnect). Groups are bounded (~256), so a message fans out to at most a few hundred recipients. I'd confirm ordering is per-conversation (not global), and that 'exactly once' really means at-least-once delivery plus client dedup by message id. Presence and receipts are separate, lighter sub-systems.
Connection layer. Clients hold a WebSocket to a stateless gateway server (one of thousands; at ~500K connections each, ~1,000 servers cover the peak). The hard part is routing: when A sends to B, A's gateway must find B's gateway. A connection registry (user_id → gateway_id, in Redis) updated on connect/disconnect, or a pub/sub channel per user that B's gateway subscribes to, does this. The sender's gateway looks up B, forwards the message to B's gateway, which pushes it down B's socket.
Data model. Messages are stored keyed by conversation_id and ordered by a time-sortable id, in a write-optimised, horizontally sharded store (Cassandra/HBase-style), sharded by conversation. For offline delivery each recipient has a mailbox/inbox (a queue of undelivered message ids); on reconnect the client drains it. Recent history is hot (cached / on fast storage); old history ages into cold archive.
Send → deliver flow. Send → server assigns a message id and persists to the conversation log → for each recipient, if online, push to their gateway; if offline, enqueue in their mailbox. Delivery is at-least-once; the client deduplicates by message id, giving exactly-once feel. Delivery and read receipts are just small messages flowing the other way, updating per-message state. The sender never blocks on fan-out — it returns once the message is durably persisted.
Storage & fan-out scaling. ~15 TB/day shards cleanly by conversation; group fan-out (1 send → up to 256 deliveries) is done asynchronously by workers reading the conversation log, so a large group never stalls the sender. Hot recent messages live in cache; archives compress to cheap storage.
Presence & failure. Presence is a heartbeat with a short TTL in an in-memory store — the client refreshes every ~30 s, and absence of a refresh means offline; we never write every status flip to a database. If a gateway crashes, its ~500K connections drop, clients reconnect to another gateway (the registry updates), and any messages sent meanwhile are already durably in each recipient's mailbox, so nothing is lost — they arrive on reconnect.
A chat system tests whether you can reason about stateful persistent connections at scale. The defining challenge is routing: with hundreds of millions of live WebSocket connections spread over thousands of stateless gateways, sending a message means finding the gateway that holds the recipient's connection — solved with a connection registry or per-user pub/sub. Durability comes from persisting every message to a conversation log plus a per-user mailbox, so offline users and gateway crashes never lose messages (at-least-once delivery + client-side dedup by message id gives exactly-once feel). Presence is deliberately made cheap with short-TTL heartbeats rather than database writes, and group fan-out is pushed off the sender's critical path.
System Design/sd-fundamentals/caching-strategies
Design a social news feed (home timeline, à la Twitter/Facebook): each user opens the app and sees a feed of recent posts from the accounts they follow.#
Show answer
Requirements. The workload is ~20:1 read-heavy, so the read (feed assembly) path is what we optimise. Ordering can be reverse-chronological to start (ranking is an orthogonal layer). The defining edge case is the follower skew: a normal post reaches ~200 timelines, a celebrity post reaches up to 100M — those need different handling.
Fan-out strategy. The core decision. Fan-out-on-write (push each post id into every follower's precomputed timeline) makes reads cheap but explodes on celebrities — one post = 100M writes. Fan-out-on-read (assemble at query time by pulling recent posts from everyone you follow) makes celebrity posts free but makes every feed read expensive. I'd use a hybrid: push on write for ordinary accounts, and for celebrities skip the push and pull their recent posts at read time, merging them into the timeline. This caps write amplification while keeping normal reads cheap.
Feed storage. Each user has a precomputed timeline: a capped list of recent post ids (say the latest few hundred) in Redis. Post bodies live once in a separate post store (sharded by post id). Storing ids — not full copies — per follower keeps the timeline small and means an edited/deleted post is resolved at hydration time.
Read path. On refresh: read the user's precomputed timeline (ids from pushed accounts) → pull recent post ids from the handful of celebrities they follow → merge and sort by time → hydrate the top N bodies from the post cache → return. Almost everything is served from cache, so p99 stays under 200 ms. The celebrity pull is bounded because a user follows only a few of them.
Bottleneck & scaling. The bottleneck is fan-out write amplification: 100M posts/day × ~200 avg followers ≈ tens of billions of timeline writes/day, and the tail (celebrities) is what kills pure push. The hybrid split removes the worst offenders; the remaining fan-out runs on async workers off a queue so posting never blocks; timelines and post bodies are cached and sharded. At higher load we add fan-out workers and cache capacity, and tune the push/pull follower threshold.
The news feed is the canonical fan-out problem, and the whole interview hinges on one decision: fan-out-on-write vs fan-out-on-read. Push (write) makes reads trivial but suffers catastrophic write amplification for high-follower accounts — one celebrity post would mean 100M timeline writes. Pull (read) makes posting cheap but every feed assembly becomes expensive. The correct answer is a hybrid: push for ordinary accounts, pull-and-merge for celebrities, which bounds the worst case on both sides. Storing post ids (not full copies) in per-user timelines, hydrating bodies from a shared cache, and running fan-out on async workers off the posting path are the supporting moves that keep the read path inside the latency budget.
Related interview questions
Job market
See system-design salaries and hiring demand from live job postings.
Practise these until they stick
That's every question we hold on this topic, and the page marks what you pick. What it can't do is remember. A free account keeps every answer, and what you miss comes back until it's right: after a day, then at longer gaps.
Free · the whole bank · 100 marked answers per 30 days · written feedback on the paid plan