Blueprint
Infrastructure advanced

Design a message queue

You're designing Kafka, and the whole question turns on one insight: the fastest database for this workload is an append-only file.

asked at LinkedIn · Uber · Confluent Frequently asked ~40 min walkthrough
01

Scope the requirements

The trap here is treating this as a CRUD problem. The interviewer wants to see whether I know that a queue at this scale is a storage-engine design question — and whether I can state the ordering and delivery guarantees precisely instead of hand-waving them.

Functional — what it must do

  • Producers publish messages to named topics; consumers subscribe and consume them.
  • Topics split into partitions; ordering is guaranteed within a partition, keyed by a partition key.
  • Consumer groups: each partition is consumed by exactly one member of a group; separate groups each see all the data.
  • Configurable retention (say two weeks) with replay — a consumer can rewind to any offset it still holds.
  • Explicitly cut: message routing rules, priority queues, per-message delay — those are RabbitMQ-shaped features and I'd say so.

Non-functional — the numbers that shape the design

  • Throughput target: ~1 GB/s sustained ingest per cluster, messages around 1 KB.
  • Durability: no acknowledged message is ever lost — replication factor 3, survive any single broker failure.
  • Delivery: at-least-once by default, with at-most-once and effectively-exactly-once as configurable options.
  • Latency vs throughput is a dial, not a fixed target — batching trades tens of ms of latency for order-of-magnitude throughput.
  • Horizontally scalable: add brokers and partitions without downtime.
Say this out loud

The founding fact of this design is that sequential disk writes run at hundreds of megabytes a second even on spinning disks. So I'm not going to build a database — I'm going to build a replicated append-only log and let the filesystem do the work.

02

Back-of-envelope estimates

The numbers here don't size a database — they size a log. What matters is bytes per second per partition, and how many partitions and disks that implies.

QuantityEstimateHow you got there
Ingest~1 GB/s~1M messages/s at ~1 KB each; this is the sustained cluster target.
Per-partition throughput~10 MB/sone partition is one sequential log on one leader — call it ~10k msg/s comfortably.
Partitions neededhundreds1 GB/s ÷ 10 MB/s = 100 minimum; provision several hundred for headroom and hot keys.
Retention storage~1.2 PB1 GB/s × 14 days ≈ 1.2 PB of raw log.
With replication ×3~3.6 PBevery partition has a leader and two followers; storage is the real cluster cost, not CPU.
Sequential disk write~500 MB/seven HDDs manage hundreds of MB/s sequentially — the number that makes the whole design work.
Say this out loud

One gigabyte a second sounds scary until you divide it: a hundred-odd partitions at ten megabytes a second each, and each partition is a plain sequential file write. The hard part isn't throughput, it's not losing data during failures.

03

Sketch the API

The API is small and looks deceptively like a function call. The interesting decisions are the acks setting on produce and the fact that consume is a pull, not a push.

RPCproduce(topic, partitionKey, message[, acks])Batched. Routes by hash(key) % partitions to the partition leader; ack semantics set by acks (0, 1, all).
RPCconsume(topic, consumerGroup)Long-poll fetch of a batch from the caller's assigned partitions, starting at its committed offset.
RPCcommit(topic, partition, offset)Records the consumer group's progress. Committing after processing gives at-least-once; before gives at-most-once.
RPCcreateTopic(name, partitions, replicationFactor)Admin plane; writes to the metadata quorum, triggers partition placement across brokers.
  • Consumers pull; the broker never pushes. Pull lets a slow consumer control its own rate — backpressure is free — and makes replay trivial because the consumer owns its offset. The cost is polling overhead, which long-polling (hold the fetch open until data arrives) reduces to nearly nothing. I'd volunteer this before being asked; push-based designs with no backpressure story are the classic failure here.
  • The offset is the whole consumption model: the broker deletes nothing on consume, it only ages data out by retention. That single choice is what separates this from RabbitMQ and enables replay, multiple independent groups, and reprocessing after a bug.
04

Data model

There is deliberately no database here. The partition log is the data model, and saying that clearly is worth more than any schema.

partition log
append-only segment files on local disk · sparse index: offset → byte position
Messages addressed by a monotonically increasing offset. Old segments are whole-file deletes at retention — no per-row cleanup.
cluster metadata
topics · partition → broker map · ISR lists · broker liveness
Lives in a Raft-based quorum (KRaft) or ZooKeeper. Small, consistent, and the source of truth for leadership.
consumer offsets
(group, topic, partition) → offset
Stored in an internal compacted topic — the system eats its own dog food rather than adding a database.
The database call

I'd store messages as raw log segments on local disks and refuse the database on principle: sequential appends, the OS page cache serving hot reads, and zero-copy sendfile from file to socket is the entire performance story — a B-tree would throw all three away. If a requirement appeared for months of retention, I'd add tiered storage — old segments offloaded to S3 with brokers serving them on demand — rather than ever reaching for a database.

05

High-level design

Brokers own partitions; each partition has one leader taking all reads and writes, with followers replicating. A small metadata quorum handles liveness and leader election, and a coordinator manages consumer-group membership.

client service data replication Producers batch + hash(key) Consumer group pull, long-poll Partition leader broker Group coordinator membership, rebalance Append-only log segments + index Metadata quorum KRaft / ZooKeeper Follower brokers ISR replicas Offsets topic compacted heartbeat produce batch append assignments replicate commits ISR status fetch(offset)
one leader per partition takes every read and write; followers replicate the log and exist only to be promoted. notice consumers pull — no arrow ever pushes data at them.
  1. A producer fetches cluster metadata once, hashes the partition key, batches messages, and sends the batch to that partition's leader broker.
  2. The leader appends the batch to its log segment — a sequential write into the page cache — and followers pull the new records to replicate.
  3. With acks=all, the leader acknowledges only once every in-sync replica has the batch; the producer's ack means "this survives a broker loss".
  4. Consumers in a group are assigned partitions by the coordinator; each long-polls its leaders with fetch(offset) and receives batches served largely from page cache via zero-copy.
  5. After processing, the consumer commits its offset to the compacted offsets topic. If a broker dies, the metadata quorum promotes an in-sync follower and clients re-fetch metadata — the log makes recovery a pointer update, not a rebuild.
06

Deep dives — where the interview is won

Phase 01

The storage engine is a file, and that's the whole trick

Every message store temptation — MySQL, Mongo, even Redis — dies on the same fact: this workload is write-once, read-sequentially, delete-by-age. An append-only segment file matches it perfectly. Writes are sequential (hundreds of MB/s per disk), reads for a caught-up consumer come straight from the OS page cache, and delivery to the socket uses zero-copy sendfile so message bytes never even enter the application's memory. Retention is dropping whole segment files — no vacuuming, no fragmentation.

The offset index is the only structure on top: a sparse file mapping offset → byte position every few KB, so a rewind seeks in two hops. I'd say explicitly that consumers being mostly caught-up is what makes this scream — the hot tail of every partition lives in RAM for free, and the design degrades gracefully to disk reads only for replays.

producer log file consumer Producer Segment file append-only Consumer offset ptr append seek + read
log is an append-only file. producers write to the end, consumers seek by offset.
Trade-offYou give up random access and secondary indexes entirely — you cannot query messages, only scan from an offset. Any 'find message where…' requirement means a downstream consumer building its own index, and that's the correct answer, not a missing feature.
Phase 02

Durability is a dial: acks and the in-sync replica set

Each partition has a leader and (with RF=3) two followers. The ISR — in-sync replica set — is the subset currently caught up. The producer chooses its guarantee: acks=0 is fire-and-forget for metrics, acks=1 means the leader has it (a small loss window if the leader dies before replication), acks=all means every ISR member has it — durable against any single failure, at the cost of the slowest in-sync follower's latency.

The subtle brilliance of the ISR is what it decouples: "replicated" no longer means "all replicas". A follower that falls behind gets evicted from the ISR, writes continue, and it rejoins when caught up. Without that, one slow disk stalls the whole partition. Leader failure is then a metadata operation: the quorum promotes an ISR member, which by definition has every acknowledged message.

producer leader isr followers Producer Leader Follower 1 Follower 2 write replicate
acks=1 fast, acks=all durable. ISR is the set that has caught up to the leader.
Trade-offIf the ISR shrinks to just the leader, acks=all silently degrades to acks=1 — so you set min.insync.replicas=2 and accept refusing writes over losing them. That's a CAP choice and I'd name it as one: for a durability product, unavailability beats data loss.
Phase 03

Exactly-once is at-least-once plus idempotence — say the mechanics

At-least-once falls out naturally: producers retry on timeout, consumers commit offsets after processing, so crashes cause reprocessing, never loss. The duplicates come from two places and each has a distinct fix. Producer-side: a retry after a lost ack re-appends the batch — fixed by an idempotent producer, where each producer gets an ID and per-partition sequence numbers so the broker discards replays. Consumer-side: crash after processing but before commit — fixed either by transactional offset commits (process and commit atomically) or, far more common in practice, an idempotent consumer that dedupes on a business key.

The interview point to land: exactly-once is not a wire protocol property, it's an end-to-end contract, and the pragmatic answer for most systems is at-least-once delivery with idempotent handlers. Claiming exactly-once without saying sequence numbers or dedupe keys is the fastest way to lose credibility here.

queue consumer app db Kafka Consumer at-least-once App DB seen(msg_id)? deliver if new → apply
consumer commits (msg_id, offset) inside its own transaction. duplicate deliveries dedupe by msg_id.
Trade-offTransactional exactly-once costs throughput and couples consumers to the queue's transaction machinery; idempotent consumers cost a dedupe table and a careful key choice. I'd default to the latter and keep the queue simple.
Phase 04

Partitions are the unit of everything — ordering, parallelism, and pain

One partition gives you total order, one consumer's worth of parallelism, and one broker's worth of throughput — so partition count is the central capacity decision. Keys with the same value always land in the same partition, which is the ordering guarantee users actually rely on: all events for one user, in order. The two failure modes are hot partitions (one celebrity key taking 10× the traffic — fix by salting the key or accepting per-entity throughput limits) and the fact that adding partitions breaks key→partition stability, reshuffling which partition a key maps to — so you over-provision partitions up front rather than resizing live.

Consumer-group rebalancing is the operational sharp edge: naive rebalance stops the world for every member whenever one joins or dies. I'd mention cooperative/sticky assignment — only the moved partitions pause — as the fix, because rebalance storms are what actually pages people at 3am.

topic partitions consumers topic 'orders' part 0 part 1 part 2 part 3 consumer A consumer B
partition = ordering unit + parallelism unit + rebalance unit. all three coupled by design.
Trade-offMore partitions buy parallelism but cost leader-election time on failover, more open files, and more metadata; a topic with 10,000 partitions makes every cluster operation slower. Provision generously but not absurdly — hundreds, not tens of thousands.
07

Follow-ups you should expect

When would you pick RabbitMQ over this design?
When the job is task distribution, not event streaming: per-message acks, routing rules, priorities, and delete-on-consume fit a work queue where each message is a job for exactly one worker. The log design wins when you need replay, multiple independent consumers of the same data, or throughput above a few tens of thousands of messages a second. They're different tools; the log is not strictly better.
Can you give me global ordering across the whole topic?
Only by using a single partition, which caps the topic at one broker's throughput and one consumer — so effectively no, at scale. The honest answer is that nobody needs global order; they need per-key order, and hash partitioning gives that for free. I'd push back on the requirement before paying for it.
A consumer keeps crashing on one message — what happens?
A poison message blocks its whole partition, because offsets commit in order. The pattern is bounded retries, then shunt the message to a retry topic with backoff, and after N attempts to a dead-letter queue with an alert — the partition keeps moving. Skipping silently is the one thing you never do.
How do you handle a 50 MB message?
I don't put it through the queue — large messages wreck batching, page-cache efficiency, and replication. The claim-check pattern: write the payload to S3, publish a small message with the pointer and metadata. The queue stays fast and the blob store does what it's good at.
Retention is two weeks but compliance wants six months. Now what?
Tiered storage: closed segments get offloaded to object storage and brokers fetch them on demand for old reads. Local disks hold the hot tail, S3 holds the archive at a tenth the cost. This is what Kafka itself added — the log abstraction doesn't change, only where segments live.

Where candidates lose the room

  • Storing messages in MySQL or MongoDB with random access — it misses the append-only insight the entire question exists to test.
  • Promising global total ordering across partitions, which quietly commits you to single-node throughput.
  • Designing push-based delivery with no backpressure story — the first slow consumer takes down the pipeline.
  • Skipping offset management: no answer for a consumer that crashes mid-batch means no delivery guarantee at all.
  • Claiming "exactly-once" as a checkbox without sequence numbers, transactions, or idempotent-consumer mechanics behind it.

Concepts this leans on

Found a bug or want more?

Tell me what's broken, missing, or wrong. Every message lands in my inbox.