Skip to the solution
MediumSystem Design13 min read

What is a message queue and why use one?

276 views

The problem

Question presented to candidate: "What is a message queue, and why would you put one between two services instead of having one call the other? Tell me what happens when a consumer crashes halfway through a message, and what can go wrong."

What a strong answer should cover:

  • A queue is a durable buffer between producers and consumers: the producer hands off work and returns; consumers pull at their own pace
  • The reasons: asynchronous work (fast user response), absorbing traffic spikes (load levelling), independent scaling and failure of each side, and competing consumers for parallelism
  • The delivery mechanics: receive, process, acknowledge/delete; an unacknowledged message comes back after a visibility timeout; repeated failures go to a dead-letter queue
  • The consequence: delivery is usually at-least-once, so consumers must be idempotent; FIFO ordering costs throughput and is usually per group
  • The costs: queue depth and consumer lag to monitor, eventual rather than immediate results, one more system to run, and a real product example (SQS defaults, RabbitMQ acks, Kafka)

Code / implementation expected: Optional β€” explanation first; the example simulates a burst, a crashed consumer, a poison message and a duplicate delivery.

  • message-queue

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

13 min read Β· 11 sections

Read the code

A message queue: burst absorption, visibility timeout, dead-letter queue, duplicate delivery
// A message queue in one process: burst absorption, visibility timeout, retries, a
// dead-letter queue and a duplicate delivery. It stands in for SQS or RabbitMQ; the clock is fake.

// ---- 1. A burst: 3 workers x 10 jobs/s against a spike ----
const ARRIVALS = [10, 10, 150, 150, 10, 10, 10, 10, 10, 10];
const CAPACITY = 30;
console.log("Part 1: 380 jobs in 10 s, workers handle " + CAPACITY + "/s");
let rejected = 0;
for (const a of ARRIVALS) rejected += Math.max(0, a - CAPACITY);
console.log("  no queue (call the service directly): " + rejected + " requests rejected or timed out");
const waiting = []; // arrival second of each queued job
let done = 0, maxWait = 0, t = 0;
const rows = [];
while (t < ARRIVALS.length || waiting.length) {
  for (let i = 0; i < (ARRIVALS[t] || 0); i++) waiting.push(t);
  let served = 0;
  while (served < CAPACITY && waiting.length) { maxWait = Math.max(maxWait, t - waiting.shift()); served++; }
  done += served;
  if (t <= 4 || waiting.length === 0) rows.push("  t=" + String(t).padStart(2) + "s arrived " + String(ARRIVALS[t] || 0).padStart(3) + ", processed " + served + ", depth " + waiting.length);
  if (waiting.length === 0 && t >= ARRIVALS.length) break;
  t++;
}
console.log("  with a queue:\n" + rows.join("\n"));
console.log("  with a queue: " + done + " done, 0 rejected, oldest job waited " + maxWait + " s");

// ---- 2. Visibility timeout, redelivery and a dead-letter queue ----
console.log("\nPart 2: visibility timeout 30 s, maxReceiveCount 3");
const VIS = 30, MAX_RECEIVES = 3;
const queue = [
  { id: "m1", body: "resize photo 1", receives: 0, visibleAt: 0 },
  { id: "m2", body: "resize photo 2", receives: 0, visibleAt: 0 },
  { id: "m3", body: "corrupt file", receives: 0, visibleAt: 0 },
];
const dlq = [];
let crashedOnce = false;
function receive(now) {
  const m = queue.find((x) => x.visibleAt <= now);
  if (!m) return null;
  m.receives++;
  if (m.receives > MAX_RECEIVES) { queue.splice(queue.indexOf(m), 1); dlq.push(m); console.log("  t=" + now + "s " + m.id + " moved to DLQ after " + MAX_RECEIVES + " failed receives"); return receive(now); }
  m.visibleAt = now + VIS; // hidden from other consumers until acked or timed out
  return m;
}
const ack = (m) => queue.splice(queue.indexOf(m), 1);
for (let now = 0; now <= 120; now += 10) {
  let m;
  while ((m = receive(now))) {
    if (m.id === "m2" && !crashedOnce) { crashedOnce = true; console.log("  t=" + now + "s worker takes m2 and crashes (no ack)"); continue; }
    if (m.id === "m3") { console.log("  t=" + now + "s m3 fails (receive " + m.receives + "), becomes visible again at t=" + m.visibleAt + "s"); continue; }
    ack(m);
    console.log("  t=" + now + "s " + m.id + " processed and acked (receive " + m.receives + ")");
  }
}
console.log("  queue empty: " + (queue.length === 0) + "; DLQ holds " + dlq.map((x) => x.id + " '" + x.body + "'").join(","));

// ---- 3. Processing longer than the visibility timeout: a duplicate ----
console.log("\nPart 3: charge job takes 40 s, visibility timeout is 30 s");
const charges = [], seenKeys = new Set();
const job = { id: "pay-17", idemKey: "order-17", amount: 25 };
function charge(worker, at, idempotent) {
  if (idempotent && seenKeys.has(job.idemKey)) return "  t=" + at + "s " + worker + " finds key " + job.idemKey + " already used, skips";
  seenKeys.add(job.idemKey); charges.push(job.amount);
  return "  t=" + at + "s " + worker + " charges $" + job.amount;
}
for (const idempotent of [false, true]) {
  charges.length = 0; seenKeys.clear();
  console.log(idempotent ? " with an idempotency key:" : " without an idempotency key:");
  console.log("  t=0s worker-1 receives pay-17; t=30s it is still working, so the message reappears");
  console.log("  t=30s worker-2 receives pay-17 (receive 2)");
  console.log(charge("worker-1", 40, idempotent));
  console.log(charge("worker-2", 70, idempotent));
  console.log("  customer charged " + charges.length + " time(s), total $" + charges.reduce((a, b) => a + b, 0));
}

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 48 of 106 in System design

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextMedium

How does caching improve system performance?

Continue