Design a distributed message queue
Build the thing most other designs on this site lean on: a durable, partitioned log that thousands of producers write to and many independent consumers read from, at millions of messages a second. Think Apache Kafka, Amazon Kinesis or Pulsar. The core idea is one sentence; the interview is about what happens when brokers fail, consumers are slow, and one key gets all the traffic.
1. The prompt
"Design a distributed message queue that services use to publish events and consume them, durably and at high throughput."
What makes it hard:
- "Durable" and "fast" pull against each other: every copy to another machine costs time.
- Consumers come and go, crash mid-batch and fall behind, and the system must neither lose nor endlessly redeliver.
- Ordering is expected, but global ordering doesn't scale.
2. Frame the problem
A work queue or an event log?
An event log: several independent consumer groups each read every message, and can re-read history.
That rules out deleting a message when it's consumed. Messages are kept for a retention period, and each group tracks its own position.
How much traffic?
About 1 million messages a second at peak, average 1 KB, so 1 GB/s in; consumers read it about three times over.
Far beyond one machine: the log must be split into partitions spread over many brokers.
How long do we keep data?
7 days by default; some topics forever (compacted).
1 GB/s × 7 days ≈ 600 TB before replication. Storage, not CPU, sizes the cluster.
Ordering?
Per key: all events for one order or one user, in order. No global order.
Messages with the same key go to the same partition; order holds within a partition.
Can we lose a message?
Not once it's acknowledged, for most topics. A few (metrics) can trade safety for speed.
Replication with a configurable acknowledgement level per producer.
Delivery guarantee to consumers?
At least once by default; exactly once for pipelines that read, transform and write back.
Consumers commit their position after processing; exactly once needs producer idempotence and transactions.
Latency?
Publish to consume in tens of milliseconds at p99.
Batching helps throughput but adds delay; we bound it by time.
3. What it must do
- A producer can publish a message (key, value) to a topic and learn when it's durably stored.
- A consumer group can read every message of a topic in order per key, sharing the work across its members.
- Messages are kept for the retention period, so a group can replay from any position.
Not now: per-message routing rules and priorities (a work queue's job), delayed delivery, schema registry, cross-region replication (a follow-up).
4. How well it must do it
| Target | Why this number |
|---|---|
| Throughput: 1 GB/s in, 3 GB/s out, growing 2× a year load shape | It's the backbone for every other team's events. |
| Durability: an acknowledged message survives the loss of any one broker (and, with a stricter setting, two) data safety | "Published" must mean published. |
| Latency: publish-to-consume under 50 ms at p99 speed | Downstream systems react to events, not hourly batches. |
| Order per key, and no message skipped by a consumer group consistency | Events for one order applied out of order corrupt state. |
| Availability: partitions keep accepting writes when a broker dies, within seconds surviving failures | Producers can buffer briefly, not for minutes. |
5. Numbers that change the design
| Quantity | Working | → so we… |
|---|---|---|
| Write throughput | 1 GB/s × replication factor 3 = 3 GB/s of disk writes; a broker sustains maybe 200–500 MB/s | → so we need tens of brokers and hundreds of partitions, sized by disk and network |
| Storage | 1 GB/s × 86,400 s × 7 days × 3 replicas ≈ 1.8 PB | Local disks get expensive → so old segments move to object storage (tiered storage) |
| Message size | 1 KB; a network round trip per message would cap a producer at a few thousand a second | → so producers batch (by size or a few ms) and compress batches |
| Partitions per topic | A partition is consumed by one member of a group; a consumer does ~10–50 MB/s | A 500 MB/s topic needs ≥ 20 consumers → so it needs ≥ 20 partitions, and more for growth |
Illustrative numbers. Skipped: the number of topics (metadata only) and producer count (connections are cheap).
6. The nouns
- Topic, split into partitions: each an append-only, ordered log.
- Message: key, value, headers, timestamp, and its offset (position) in the partition.
- Broker: a server holding partition replicas; one replica of each partition is the leader.
- Consumer group and its committed offset per partition.
- Controller: the metadata authority (which broker leads which partition).
partition topic, id, leader_broker, replicas[], in_sync_replicas[], leader_epoch segment files partition/00000000000004180000.log + .index (offset → file position) + .timeindex message offset, timestamp, key, value, headers (inside compressed batches) offsets (group, topic, partition) → committed_offset (itself stored in an internal, compacted topic)
7. The contract
Binary protocol over long-lived TCP (not REST: per-message HTTP is too costly)
Produce {topic, partition (or key), batch[{key, value, headers}], acks: 0 | 1 | all}
← {base_offset} | error (NOT_LEADER → refresh metadata and retry)
Fetch {topic, partition, from_offset, max_bytes, max_wait_ms} (long poll)
← batches
CommitOffset {group, topic, partition, offset}
JoinGroup / Heartbeat / LeaveGroup (group membership)
Metadata {topics} ← partition → leader map
Producers send to the partition leader directly; the key's hash picks the partition. A producer id plus a sequence number per partition lets the broker drop duplicate retries (idempotent producer).
Data flow: producer batches by partition → leader appends to its log → followers fetch and append → leader acknowledges per acks → consumers fetch from their committed offset → process → commit the next offset.
8. First working version
Brokers that append to partition logs on disk, a controller that knows the leaders, producers that send to leaders, consumer groups that pull.
8.1 Publishing
An append is a sequential write to the end of the current segment file: the operating system's page cache absorbs it and the disk sees large sequential writes, which is why one broker can take hundreds of MB a second.
8.2 Consuming as a group
Partitions are divided among the group's members; each partition has exactly one reader in the group, so order per partition is kept. The consumer commits the next offset after processing: if it crashes before committing, the next owner re-reads from the last commit (at least once).
8.3 Keeping and deleting
Retention deletes whole old segment files, never single messages: cheap and fast. Consumers can rewind to any retained offset to replay.
9. Make it hold up
9.1 Replication and acknowledgements durability · availability
| Approach | Why | |
|---|---|---|
| ✗ Bad | One copy per partition, on a RAID disk | A dead machine takes its partitions offline, maybe forever. |
| ~ Good | Leader plus followers; acknowledge when the leader has written (acks=1) | Fast, but a leader that dies before followers copy the last messages loses messages the producer was told were safe. |
| ✓ Great | Leader-follower with an in-sync replica set (ISR): followers pull from the leader; a follower that falls behind leaves the ISR; with acks=all the leader acknowledges once every ISR member has the message, and min.insync.replicas=2 refuses writes if fewer than two copies would exist | No acknowledged message is lost while one in-sync copy survives. Only in-sync replicas can become leader. Producers that can afford loss still choose acks=1 for speed. |
Try the trade-off in the lab in section 13: crash the leader after message 7 with each setting.
9.2 Who decides who leads availability · consistency
A controller holds the cluster metadata: partitions, replicas, ISR, leaders. It must itself be fault tolerant and agree on one truth: Kafka moved this from ZooKeeper to a built-in Raft quorum of controllers (KRaft). When a broker dies, the controller picks a new leader from the ISR for each of its partitions and bumps the partition's leader epoch; brokers and clients reject requests carrying an old epoch, so a returning old leader can't accept writes (fencing, Module 21). Rule to state: an out-of-sync replica must never become leader unless you explicitly accept data loss ("unclean" election).
9.3 Consumer groups and rebalancing no skipped messages
| Approach | Why | |
|---|---|---|
| ✗ Bad | Consumers pick partitions themselves | Two readers on one partition, or none. |
| ~ Good | A group coordinator; on any join or leave, everyone stops, gives up partitions and gets a new assignment | Correct, but every deploy of a 100-member group pauses all consumption ("stop the world"). |
| ✓ Great | Incremental (cooperative) rebalancing with sticky assignment: only partitions that must move are revoked; static membership so a restarting consumer reclaims its partitions without a rebalance | Deploys and scaling cause small, local pauses instead of group-wide ones. |
9.4 A hot key throughput · latency
All messages with one key go to one partition, so a celebrity user or a huge customer can overload one partition while others idle. Options: spread that key over several partitions with a suffix (losing per-key order unless you re-merge), give big tenants their own topics, or rely on the producer's partitioner only for keys that truly need order and use round-robin for the rest. Watch per-partition bytes in, not just the topic total.
9.5 Duplicates and exactly once consistency
| Approach | Why | |
|---|---|---|
| ✗ Bad | Commit offsets before processing | A crash after the commit skips messages: at most once. |
| ~ Good | Commit after processing; make consumers idempotent (dedupe by a message id, upserts) | At least once with harmless duplicates: the right default. |
| ✓ Great | Idempotent producers plus transactions for read-process-write pipelines: the output messages and the input offsets are committed atomically; readers skip uncommitted data | Exactly once within the log. Effects outside it (an email, a payment call) still need idempotency keys. |
9.6 A petabyte of retention cost
Keep recent segments on brokers' local disks for fast reads, and move older ones to object storage (D35); brokers fetch them back when a consumer replays history. Brokers become lighter, so adding or replacing one moves little data. For "latest value per key" topics (account settings, a table's changelog), use log compaction: old values for a key are removed in the background, keeping only the latest.
9.7 The whole design after the deep dives
10. Interviewer follow-ups
A consumer group is hours behind.
Lag (log end minus committed offset) is the alert. Scale out consumers up to the partition count; beyond that, more partitions are needed, which changes key placement, so plan partition counts ahead. Check whether processing, not fetching, is the bottleneck.
We need more partitions for an existing topic.
Adding partitions is easy but changes which partition a key hashes to, breaking per-key order across the change. Either over-provision partitions up front, or create a new topic and migrate consumers deliberately.
Replicate to another region.
Asynchronous mirroring of topics to a second cluster, with offset translation for consumers that fail over. Accept a small loss window, or run synchronous replication across nearby zones only.
One message is malformed and the consumer crashes on it forever.
After a few attempts, publish it to a dead-letter topic with the error and commit past it, so one poison message doesn't stop the partition.
Why do consumers pull instead of the broker pushing?
Each consumer reads at its own pace, batches naturally, and can rewind. Push needs per-consumer buffers and backpressure in the broker.
Why is it fast with ordinary disks?
Sequential appends, the page cache, batching and compression, and sending file data straight to the socket without copying through the application (zero copy).
Delete one user's data (privacy request).
Logs are append-only; options are short retention, compaction with a tombstone for the user's key, or encrypting each user's data with their own key and deleting the key.
Ten times the traffic.
More brokers and partitions, rebalanced gradually; tiered storage so new brokers don't copy history; quotas per client so one team can't starve others.
11. When things break
| Failure | What users see | How we detect it | What limits the damage |
|---|---|---|---|
| Broker dies | Seconds of retries for its partitions | Controller heartbeats | New leaders from the ISR; idempotent retries |
| Follower falls behind | Nothing; then writes refused if too few in sync | ISR shrink metrics | min.insync.replicas protects acknowledged data |
| Controller quorum loses a node | Nothing | Quorum health | Raft majority keeps metadata available |
| Consumer crash | Delay, some duplicates | Group heartbeats | Rebalance; resume from last commit |
| Disk full | Writes fail on that broker | Disk usage alerts | Retention by size, tiered storage, quotas |
| Hot partition | High latency for one key's events | Per-partition throughput | Key splitting, dedicated topics |
12. Wrap-up and the clock
Trade-offs we made:
- A log, not a queue: replay and many readers, at the cost of storage.
- Order per partition only.
- Acknowledgement level chosen per producer: safety or latency.
- At least once by default; exactly once only inside the log.
- Tiered storage: cheap history, slower replays.
What we'd watch: bytes in and out per broker and partition · under-replicated partitions · ISR shrink/expand rate · produce and fetch latency p99 · consumer lag per group · rebalance count and duration.
At 10×: more brokers and partitions, client quotas, tiered storage everywhere; the protocol and design stay the same.
A 60-minute interview
| 0–5 | Frame: log or queue, throughput, retention, ordering |
| 5–10 | Must do, how well, numbers |
| 10–15 | Nouns, protocol, data flow |
| 15–25 | First version: produce, consume, retain |
| 25–50 | Deep dives: replication (9.1), leadership (9.2), groups (9.3) |
| 50–60 | Exactly once, follow-ups, wrap-up |
A 90-minute interview
| 0–8 | Frame, including delivery guarantees |
| 8–15 | Must do, how well, numbers |
| 15–22 | Nouns, protocol, data flow |
| 22–35 | First working version |
| 35–75 | Deep dives 9.1–9.6 and the final picture |
| 75–85 | Follow-ups: lag, repartitioning, other regions |
| 85–90 | Trade-offs, metrics, 10× |
| Level | What a strong answer shows |
|---|---|
| Mid-level | Topics and partitions, an append-only log, consumer groups with offsets, replication to followers. |
| Senior | ISR and acks with min.insync.replicas, leader election with epochs, rebalancing, at-least-once with idempotent consumers, hot keys. |
| Staff+ | Why sequential I/O and batching make it fast, exactly-once and its limits, tiered storage economics, partition planning for growth, multi-tenant quotas, cross-region trade-offs. |
Common mistakes:
- Deleting messages when consumed (that's a work queue, not a log).
- Promising global ordering.
- Acknowledging before replication and calling it durable.
- Committing offsets before processing.
- Ignoring rebalances and hot keys.
13. Try it, check yourself, read more
Acknowledgement lab. A producer sends messages 1 to 10 to a partition with one leader and two followers. Followers copy with a delay of two messages. The leader crashes right after message 7. Which acknowledged messages survive the failover?
Model: the follower that becomes leader is the most up to date one; messages it doesn't have are gone. Outlined = acknowledged to the producer. Illustrative.
How does a partitioned log keep order for one customer's events?
A leader dies and acknowledged messages are lost. What was the likely setting?
When should a consumer commit its offset for at-least-once delivery?
What caps how many consumers in one group can work in parallel on a topic?
| Resource | Why |
|---|---|
| Kafka documentation: design | Persistence, replication, ISR and delivery semantics, from the source. |
| KIP-500 | Why Kafka replaced ZooKeeper with its own Raft-based controller quorum. |
| KIP-405: tiered storage | Moving old segments to object storage. |
| Exactly-once semantics in Kafka (Confluent) | Idempotent producers and transactions, explained by their designers. |