Requirements & scope
Problem Statement & Requirements
Design a distributed message queue system that decouples producers from consumers, enables asynchronous processing, guarantees message delivery, and scales to handle millions of messages per second — similar to Apache Kafka, AWS SQS, or RabbitMQ.
Functional Requirements
- Producers publish messages to named topics
- Consumers subscribe to topics and receive messages in order
- Support consumer groups — multiple consumers sharing the load of a topic, each message delivered to exactly one consumer in the group
- Messages are persisted on disk, not lost on broker failure
- Support at-least-once, at-most-once, and exactly-once delivery semantics (configurable)
- Consumers can replay messages from a specific offset (time-travel)
- Support message retention by time (e.g., 7 days) or size (e.g., 1 TB per topic)
- Ordering guarantees within a partition (not across partitions)
- Dead letter queue (DLQ) for messages that fail processing after N retries
Non-Functional Requirements
- High throughput — handle millions of messages/second per cluster
- Low latency — end-to-end publish-to-consume in < 10 ms (p99) for real-time use cases
- Durability — zero message loss once acknowledged by the broker
- High availability — survive broker failures, rack failures, even AZ failures
- Horizontal scalability — add brokers to increase throughput linearly
- Fault tolerance — automatic leader election and partition rebalancing on failure
Out of Scope
- Complex message routing (fanout, topic exchanges like RabbitMQ — simplified to topic-partition model)
- Message transformation / stream processing (that's Kafka Streams / Flink territory)
- Multi-tenancy and authentication (simplify for interview)
Scale estimations
Scale Estimations
Traffic
| Metric | Value |
|---|---|
| Messages produced / second | 1M (peak: 3M) |
| Average message size | 1 KB |
| Messages consumed / second | 3M (each message consumed by ~3 consumer groups on average) |
| Number of topics | 10,000 |
| Number of partitions (total) | 100,000 |
| Number of producers | 50,000 |
| Number of consumer groups | 5,000 |
Storage
| Metric | Value |
|---|---|
| Data ingested / second | 1M × 1 KB = 1 GB/s |
| Data ingested / day | 1 GB/s × 86,400 = ~86 TB/day |
| Retention period | 7 days |
| Raw storage (7 days) | 86 TB × 7 = ~600 TB |
| Replication factor | 3 |
| Total storage (with replication) | 600 TB × 3 = ~1.8 PB |
| Storage per broker (50 brokers) | 1.8 PB / 50 = ~36 TB per broker |
Bandwidth
| Metric | Value |
|---|---|
| Incoming (produce) | 1 GB/s |
| Outgoing (consume, 3 consumer groups avg) | 3 GB/s |
| Replication traffic (2 replicas × 1 GB/s) | 2 GB/s |
| Total network throughput | ~6 GB/s cluster-wide |
| Per broker (50 brokers) | ~120 MB/s per broker |
Hardware Estimate (per broker)
| Component | Spec |
|---|---|
| CPU | 16-24 cores (mostly I/O bound) |
| RAM | 64-128 GB (OS page cache is critical) |
| Disk | 8× 4 TB NVMe SSDs in JBOD (32 TB usable) |
| Network | 10 Gbps NIC (25 Gbps preferred) |
| Brokers in cluster | 50-100 |
Layered architecture
High-Level Architecture
High-Level ArchitectureExcalidraw diagram · editable shapes · reveal step by stepExplore
Component Breakdown
Component BreakdownExcalidraw diagram · editable shapes · reveal step by stepExplore
Key: P0-L = Partition 0 Leader, P0-F = Partition 0 Follower
API & contracts
API Design
Producer API
POST /api/v1/topics/{topic_name}/messages
Content-Type: application/json
Request:
{
"key": "user-123", // partition key (optional — null = round-robin)
"value": "base64-encoded-payload", // the message body
"headers": { // optional metadata
"correlation-id": "req-abc",
"content-type": "application/json"
},
"partition": 3, // optional — explicit partition override
"timestamp": 1712150400000 // optional — defaults to broker time
}
Response (202 Accepted):
{
"topic": "user-events",
"partition": 3,
"offset": 847291,
"timestamp": 1712150400000
}Consumer API
GET /api/v1/topics/{topic_name}/messages?group_id=analytics-pipeline
&max_messages=500
&timeout_ms=5000
Response (200 OK):
{
"messages": [
{
"key": "user-123",
"value": "base64-encoded-payload",
"headers": {"correlation-id": "req-abc"},
"partition": 3,
"offset": 847291,
"timestamp": 1712150400000
},
...
]
}Commit Offset (Acknowledge)
POST /api/v1/topics/{topic_name}/offsets
Content-Type: application/json
Request:
{
"group_id": "analytics-pipeline",
"offsets": [
{"partition": 0, "offset": 12345},
{"partition": 3, "offset": 847292}
]
}
Response: 204 No ContentAdmin API
POST /api/v1/topics — Create topic (name, partitions, replication_factor, retention)
DELETE /api/v1/topics/{topic_name} — Delete topic
PUT /api/v1/topics/{topic_name}/config — Update retention, compaction, etc.
GET /api/v1/topics/{topic_name}/info — Topic metadata (partitions, leaders, ISR)
POST /api/v1/topics/{topic_name}/partitions — Add partitions (cannot decrease)SDK-Level API (Internal Binary Protocol — High Performance)
// Producer (batch-oriented)
producer.send(topic, key, value, headers) → Future<RecordMetadata>
producer.flush() → blocks until all buffered messages sent
// Consumer (poll-based)
consumer.subscribe(topics, group_id)
records = consumer.poll(timeout_ms) → batch of ConsumerRecords
consumer.commitSync(offsets) → block until offsets durably stored
consumer.commitAsync(offsets, callback) → fire-and-forget with callback
consumer.seek(partition, offset) → rewind/fast-forward to specific offsetData model
Data Model
Core Abstractions
Core AbstractionsExcalidraw diagram · editable shapes · reveal step by stepExplore
Message Record (On-Disk Format)
Message Record (On-Disk Format)Excalidraw diagram · editable shapes · reveal step by stepExplore
Log Segment (On-Disk Storage Unit)
Partition Directory: /data/user-events-0/
00000000000000000000.log ← active segment (append-only)
00000000000000000000.index ← sparse offset → file position index
00000000000000000000.timeindex← timestamp → offset index
00000000000065536000.log ← rolled segment (immutable)
00000000000065536000.index
00000000000065536000.timeindex
Segment rolls when:
- Size exceeds segment.bytes (default 1 GB)
- Time exceeds segment.ms (default 7 days)
- Index file is fullOffset Storage
Offset StorageExcalidraw diagram · editable shapes · reveal step by stepExplore
Core Design Decisions — The Storage Engine
The most critical design decision in a message queue is how messages are stored and retrieved. This determines throughput, latency, durability, and operational complexity.
Option 1: Append-Only Log on Disk (Kafka's Approach)
Option 1: Append-Only Log on Disk (Kafka's Approach)Excalidraw diagram · editable shapes · reveal step by stepExplore
Why sequential I/O matters:
| I/O Pattern | HDD | SSD |
|---|---|---|
| Sequential write | 100-200 MB/s | 500-3000 MB/s |
| Random write | 0.1-1 MB/s | 50-200 MB/s |
| Sequential read | 100-200 MB/s | 500-3000 MB/s |
| Random read | 0.1-1 MB/s | 50-200 MB/s |
Sequential I/O is 100-1000× faster than random I/O. By only appending, Kafka achieves disk throughput that rivals network throughput.
Zero-copy transfer (sendfile):
Traditional path: Zero-copy path:
Disk → Kernel buffer Disk → Kernel buffer
Kernel buffer → User buffer ↓ (sendfile syscall)
User buffer → Socket buffer Kernel buffer → NIC buffer
Socket buffer → NIC buffer
4 copies, 4 context switches 2 copies, 2 context switches
~60% of CPU on data copies ~0% CPU on data copies| Pros | Cons |
|---|---|
| Extremely high throughput (millions msg/sec) | No per-message deletion (retention-based only) |
| Sequential I/O leverages OS page cache | Messages are immutable once written |
| Zero-copy transfers to consumers | Consumer must track own offset |
| Simple — append + read, no complex data structures | Ordering only within a partition |
Option 2: In-Memory Queue with WAL (RabbitMQ-style)
Option 2: In-Memory Queue with WAL (RabbitMQ-style)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Very low latency (microsecond for in-memory) | Memory-limited — can't buffer large backlogs |
| Per-message ACK and deletion | Complex — memory management, overflow to disk |
| Flexible routing (exchanges, bindings) | Lower throughput at scale vs log-based |
| Push-based delivery | No message replay after consumption |
Option 3: Database-Backed Queue (SQS-style)
Option 3: Database-Backed Queue (SQS-style)Excalidraw diagram · editable shapes · reveal step by stepExplore
| Pros | Cons |
|---|---|
| Fully managed (SQS), no operations | Lower throughput (1000s msg/s vs millions) |
| At-least-once with visibility timeout | No ordering guarantees (SQS standard) |
| Per-message deletion | Higher latency (database round-trips) |
| Simple operational model | No replay capability |
Recommendation
Use Option 1 (Append-Only Log) for a high-throughput, scalable message queue:
- Sequential I/O + zero-copy = unmatched throughput
- Persistent storage = unlimited retention and replay
- Simple broker logic = operational simplicity
- Consumer offset tracking = flexible consumption patterns
Core design decisions
Workshop note · added for the website’s common reading format
Use the detailed source walkthroughs in this article to examine Distributed Message Queue at this layer. State the requirement, propose the smallest component that satisfies it, and make its cost and failure mode explicit.
Request flows
Detailed Flow Diagrams
Produce Flow
Produce FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Consume Flow (Pull-Based)
Consume Flow (Pull-Based)Excalidraw diagram · editable shapes · reveal step by stepExplore
Consumer Rebalance Flow
Consumer Rebalance FlowExcalidraw diagram · editable shapes · reveal step by stepExplore
Performance & caching
Log Compaction (Bonus Deep Dive)
Log Compaction (Bonus Deep Dive)Excalidraw diagram · editable shapes · reveal step by stepExplore
Advanced design
Replication & Consistency
ISR (In-Sync Replicas)
ISR (In-Sync Replicas)Excalidraw diagram · editable shapes · reveal step by stepExplore
Acks Configuration & Durability Tradeoffs
| acks Setting | Behavior | Durability | Latency | Throughput |
|---|---|---|---|---|
acks=0 |
Producer doesn't wait for any ACK | Lowest — fire and forget | ~0.5 ms | Highest |
acks=1 |
Wait for leader to write to local log | Medium — lost if leader dies before replication | ~2-5 ms | High |
acks=all |
Wait for all ISR replicas to write | Highest — survives any single broker failure | ~5-15 ms | Lower |
Recommendation: acks=all with min.insync.replicas=2 for production:
- Tolerates 1 broker failure without data loss
- Blocks produces if ISR shrinks below 2 (prevents single-replica writes)
Leader Election
Leader ElectionExcalidraw diagram · editable shapes · reveal step by stepExplore
Partitioning Strategy
How Messages Are Routed to Partitions
How Messages Are Routed to PartitionsExcalidraw diagram · editable shapes · reveal step by stepExplore
Partition Count Selection
Target throughput: 1 GB/s
Single partition throughput:
- Producer: ~10-50 MB/s per partition
- Consumer: ~20-100 MB/s per partition (consumer is usually faster)
- Bottleneck is typically producer side
Calculation:
Required partitions = Target throughput / Per-partition throughput
= 1 GB/s / 30 MB/s (conservative)
= ~34 partitions per topic (round up to 36-48)
Rule of thumb:
- Start with max(expected_throughput / 30MB, num_consumers_in_largest_group)
- Can always ADD partitions later (cannot remove)
- More partitions = more parallelism BUT more open file handles,
longer leader election, more memory for consumer offset trackingPartition-to-Consumer Assignment Strategies
| Strategy | How It Works | Pros | Cons |
|---|---|---|---|
| Range | Assign consecutive partitions to consumers (P0-P2→C1, P3-P5→C2) | Preserves key locality for co-partitioned topics | Uneven if partition count not divisible by consumers |
| Round-Robin | Distribute partitions one by one (P0→C1, P1→C2, P2→C1...) | Even distribution | Breaks key co-partitioning |
| Sticky | Like round-robin but minimizes reassignment during rebalance | Fewer partition movements on rebalance | Slightly more complex |
| Cooperative Sticky | Incremental rebalance — only moves partitions that need moving | Near-zero downtime rebalancing | Requires multiple rebalance rounds |
Recommendation: Cooperative Sticky Assignor — minimizes stop-the-world rebalancing.
Edge cases
Handling Edge Cases
Consumer Lag & Backpressure
Problem: Consumer is slower than producer → unbounded lag
Monitoring:
consumer_lag = latest_offset(partition) - committed_offset(consumer_group, partition)
Mitigations:
1. Scale out consumers (up to # of partitions)
2. Increase consumer fetch size (fetch.max.bytes)
3. Tune consumer processing (batch DB writes, parallelize)
4. Add partitions to topic (+ proportional consumers)
5. Alert when lag exceeds threshold (e.g., > 100K messages)Exactly-Once Semantics (EOS)
Exactly-Once Semantics (EOS)Excalidraw diagram · editable shapes · reveal step by stepExplore
Dead Letter Queue (DLQ)
Dead Letter Queue (DLQ)Excalidraw diagram · editable shapes · reveal step by stepExplore
Message Ordering Across Partitions
Problem: User actions must be ordered, but topic has 12 partitions
Solution: Use the entity ID as the partition key
- key = "user-123" → hash("user-123") % 12 = partition 7
- ALL events for user-123 go to partition 7
- Within partition 7, ordering is guaranteed
Caveat: If you add partitions, hash changes → key mapping shifts
- Mitigate: Don't add partitions to topics where key ordering matters
- Or: use a custom partitioner with a stable mapping tableLarge Messages
Large MessagesExcalidraw diagram · editable shapes · reveal step by stepExplore
Tradeoffs
Tradeoffs & Design Decisions Summary
| Decision | Option A | Option B | Chosen | Why |
|---|---|---|---|---|
| Storage Engine | Append-only log (Kafka) | In-memory + WAL (RabbitMQ) | Append-only log | Sequential I/O = highest throughput; unlimited retention; replay capability |
| Delivery Model | Pull (consumer polls) | Push (broker pushes) | Pull | Consumer controls pace; no backpressure on broker; natural batching |
| Metadata Store | ZooKeeper (external) | KRaft (self-managed) | KRaft | Removes external dependency; simpler operations; better scaling (>200K partitions) |
| Ordering Scope | Global ordering | Per-partition ordering | Per-partition | Global ordering = 1 partition = no parallelism; per-partition is the right balance |
| Consumer Offset | Broker tracks (auto) | Consumer commits (manual) | Manual commit | Application controls exactly-once semantics; no re-processing on crash |
| Acks | acks=1 (leader only) | acks=all (full ISR) | acks=all | Data loss is unacceptable for most use cases; latency cost is acceptable (5-15 ms) |
| Replication | Synchronous | Asynchronous | Sync for ISR | Async loses data on leader failure; sync ISR guarantees committed = durable |
| Rebalancing | Stop-the-world | Cooperative incremental | Cooperative | Stop-the-world pauses ALL consumers during rebalance; cooperative only moves affected partitions |
| Compaction | Delete old segments | Log compaction (keep latest per key) | Both | Delete for event streams; compaction for changelog/state topics |
| Wire Protocol | HTTP/REST | Custom binary (TCP) | Binary | Lower overhead, better batching, connection multiplexing; REST for admin only |
Reliability & fault tolerance
Reliability & Fault Tolerance
Single Points of Failure & Mitigations
Single Points of Failure & MitigationsExcalidraw diagram · editable shapes · reveal step by stepExplore
Graceful Degradation
Scenario: Broker goes down (1 of 50)
1. Controller detects missing heartbeat (18s timeout)
2. For each partition where dead broker was leader:
→ Elect new leader from ISR
→ Update metadata, notify clients
3. For each partition where dead broker was follower:
→ Remove from ISR
→ Continue with remaining ISR
4. When broker returns:
→ Fetches missed data from current leaders
→ Rejoins ISR after catching up
→ Preferred leader election restores original topology
Impact: ~5-30s of unavailability for affected partitions
Other partitions continue unaffected
Zero data loss (acks=all with min.insync.replicas=2)Data Durability Guarantees
Data Durability GuaranteesExcalidraw diagram · editable shapes · reveal step by stepExplore
Production architecture
Full System Architecture (Production-Grade)
Full System Architecture (Production-Grade)Excalidraw diagram · editable shapes · reveal step by stepExplore
Further exploration
Workshop note · added for the website’s common reading format
Rebuild Distributed Message Queue from memory, then change one assumption: ten times more traffic, a new region, or a stricter consistency requirement. Which component must change first—and which does not?
Compare Leader brokers with the same boundary in a related design. Write down one alternative you rejected, what it would simplify, and when you would choose it instead. Follow the source link at the end of this article to explore the original document.
Interview playbook
Interview Tips
Start with the append-only log insight — This is the foundational design decision. Explain why sequential I/O and zero-copy make disk-based queues faster than in-memory alternatives at scale.
Draw the partition model early — Topics → Partitions → Segments. This shows you understand the parallelism model. Emphasize: ordering is per-partition, not per-topic.
Discuss the acks tradeoff in depth —
acks=0vsacks=1vsacks=all. Connect it to ISR andmin.insync.replicas. This is a nuanced durability vs latency discussion interviewers love.Explain consumer groups and rebalancing — How partitions are assigned, what triggers a rebalance, why cooperative sticky is preferred over stop-the-world. Mention the "consumers ≤ partitions" constraint.
Address exactly-once semantics — Most candidates only know at-least-once. Explaining idempotent producers (PID + sequence number) and transactions sets you apart.
Mention KRaft over ZooKeeper — Shows you know the modern architecture. ZooKeeper was a bottleneck at >200K partitions. KRaft uses Raft consensus built into the brokers themselves.
Discuss operational concerns — Partition count selection, when to add partitions (and why you can't remove them), retention policies, monitoring consumer lag, dealing with hot partitions.
Claim-check pattern for large messages — Shows pragmatism. Don't just increase
max.message.bytes— explain the S3 reference pattern.Compare with alternatives when asked — Kafka (high throughput, log-based) vs RabbitMQ (flexible routing, push-based) vs SQS (managed, simple). Each has its sweet spot.
End with scale numbers — "A single Kafka cluster with 50 brokers can handle 1M+ messages/sec, store petabytes with 7-day retention, and serve hundreds of consumer groups simultaneously."