📨 System Design distributed message queue
📨 Part II · Building Blocks · chapter 9 / 24

The log that
decouples everything

A distributed message queue is, at heart, one stubbornly simple idea: writers append messages to the end of a never-ending file, and readers each remember how far they've read. This chapter builds a mini-Kafka from that single primitive — partitions, replication, consumer groups, and the delivery guarantees that fall out of it.

Every chapter before this one peeled a job off the single box. This one zooms into the humble queue we added in Chapter 1 and asks: what if that buffer became the system's backbone — durable, replayable, and able to feed a hundred independent readers at once?

That system exists, and you've used it without knowing: Kafka, Pulsar, Kinesis, and their kin move the world's event streams. They look intimidating from the outside — partitions, ISRs, KRaft, exactly-once. But underneath there is one data structure doing almost all the work: an append-only log. A file you only ever add to the end of, and that you read by remembering a position.

We'll build the whole thing up from that primitive. First why a queue earns its place at all, then the core abstractions, then how the log lives on disk, how it survives a machine dying, how many readers share it, what "delivered" really means, and finally who keeps the cluster honest. Plain English first; the Kafka vocabulary arrives only once the idea behind it already makes sense.

1Why a queue at all

Imagine your web app, the moment a user clicks "post." It needs to: save the post, update twelve followers' timelines, generate a thumbnail, run a spam check, and fire an analytics event. Do all of that inline and the user stares at a spinner for two seconds. Worse, if the analytics service is down, the whole click fails — even though analytics had nothing to do with whether the post saved.

The fix is to decouple. The web app does the one essential thing (save the post) and then drops a little "this happened" message onto a shared list. Other services read from that list and do their slow or optional work whenever they're ready. The writer doesn't know or care who reads, or when, or how many readers there are. That shared list is the queue, and three big wins fall out of it:

  • Decoupling. Producers and consumers don't call each other directly. You can add a new consumer (a fraud detector, say) tomorrow without touching the producer at all.
  • Absorbing spikes. If a million events arrive in a minute, the queue swells and the consumers drain it steadily. The slow part never knocks over the fast part — this is backpressure handled gracefully instead of a cascade of timeouts.
  • Async work & replay. Work that can wait, waits. And — the part that makes this chapter different — because the messages stay after they're read, you can rewind and reprocess history: replay a day's events into a brand-new service, or recover from a bug by re-running the stream.

That last point is the crucial distinction. A traditional message broker (think old-school RabbitMQ semantics) treats a message like a letter: once a consumer takes it, it's gone. A log-based queue like Kafka treats messages like entries in a ledger: reading one doesn't erase it. A reader just moves its bookmark forward. Many independent readers can hold many different bookmarks in the same log at the same time.

λ
The FP framing: a log-based queue is an immutable, append-only stream. Producers append; consumers fold over the stream from wherever their cursor sits. Because the stream never mutates, two folds can run independently and a fold can be re-run from the start — that's replay, for free, from immutability.

2Core abstractions — topics, partitions, offsets

Three nouns carry the whole model. Learn them and the rest is detail.

  • A topic is a named stream of messages — "orders", "clicks", "signups". It's the logical channel producers write to and consumers read from.
  • A partition is one physical append-only log. A topic is split into several partitions so the load can spread across machines. Each partition is an independent ordered sequence; the topic is just the bag of its partitions.
  • An offset is a message's position number within a partition — 0, 1, 2, 3… counting up forever. A consumer's entire memory of "where am I" is a single offset per partition. That's it. A consumer is a number that knows how to go up.

The cast of characters: a producer sends messages to a topic; a broker is a server that hosts partitions and stores their logs on disk; a consumer reads messages starting from an offset. Producer → broker → consumer, with the broker's append-only log as the thing in the middle that everyone agrees on.

The genius is what's missing. The broker keeps no per-consumer state about what's been delivered or acknowledged — it just stores bytes in order. All the "where have I read to" lives on the consumer side as an offset. That asymmetry is why one Kafka cluster can fan one topic out to hundreds of consumers cheaply: the broker isn't tracking any of them.

Interactive · partitioned-log writer Same key → same partition; consumers hold their own offsets
partitions
3
total messages
0
consumer A lag
0
consumer B lag
0
🔑
Notice in the widget: alice always lands in the same partition, because the partition is chosen by hash(key) % partitionCount. That determinism is what gives you per-key ordering — the topic of the next section.

3Partitioning & ordering

Here is the single most misunderstood fact about Kafka, so we'll say it plainly: order is guaranteed only within one partition, never across the whole topic. Inside partition 2 the messages are 0, 1, 2, 3 in iron sequence. But message 5 in partition 0 and message 5 in partition 1 have no defined order relative to each other — they were written in parallel by different machines.

So how do you make sure related messages stay in order? You give them the same partition key. Every message can carry a key, and the broker routes it by hash(key) % partitionCount. Use user_id as the key and every event for a given user flows through one partition, in order. Different users may scatter across partitions, but each user's own timeline is perfectly sequenced. You get ordering exactly where you need it and parallelism everywhere else.

And parallelism is the whole point of partitions. The number of partitions is your unit of parallelism: a topic with 12 partitions can be drained by up to 12 consumers at once, each owning some partitions. Too few partitions and you can't add readers to go faster; too many and you pay overhead. Partition count is one of the few Kafka decisions that's genuinely hard to change later, so it's worth getting roughly right up front.

⚠️
The null-key trap: produce with no key and messages are spread round-robin across partitions for balance — which means no ordering guarantee between them. If two events must be processed in order, they must share a key. "Why are my events out of order?" is almost always a missing or wrong key.

4Storage on disk — why a log is fast

"Writing to disk is slow" is true for random writes — seeking the disk head all over the place. But a log only ever appends to the end, which is a sequential write, and sequential disk throughput is enormous (often faster than random RAM access, once you count it in megabytes per second). Choosing append-only isn't just for replay — it's what makes the log fast in the first place.

A partition's log isn't one giant file; it's a series of segment files of a fixed size. New messages go into the active segment; when it fills, the broker rolls to a fresh one. This makes deletion trivial: to enforce retention (keep the last 7 days, or the last 50 GB), you just drop whole old segment files — no expensive in-place edits.

Two more tricks make reads cheap:

  • The page cache. Recently written messages are still sitting in the OS's file cache in RAM. A consumer reading near the tail of the log — which is most consumers, most of the time — is served straight from memory without touching the physical disk at all.
  • Zero-copy. To send a chunk of log to a consumer, the broker can ask the kernel to splice bytes directly from the page cache to the network socket (via sendfile), skipping the usual copies through application memory. Less CPU, less garbage, more throughput.

Finally, log compaction is an alternative to time/size retention for "latest value" topics. Instead of deleting old messages by age, the broker keeps only the most recent message for each key and garbage-collects the earlier ones. A compacted topic becomes a durable, replayable snapshot of current state — perfect for "the current address of every user" rather than "every address change ever."

λ
The FP framing: the full log is your event history (an immutable list of changes); a compacted log is the fold of that history keyed by id — the current state, with the intermediate steps thrown away. Same data, two views: the changelog and the materialized snapshot.

5Replication & durability — surviving a dead broker

A log on one disk dies with that disk. So each partition is copied onto several brokers. One copy is the leader; the rest are followers. All reads and writes for a partition go to its leader; the followers do nothing but continuously pull the leader's new messages and append them to their own copies, racing to stay caught up.

The followers that are caught up (within a small allowed gap) form the in-sync replica set, the ISR. The ISR is the live roster of copies that are trustworthy enough to take over. If a follower falls too far behind or its broker hangs, it's kicked out of the ISR; when it catches up again, it rejoins. The ISR shrinks under stress and grows back as things recover.

Now the durability dial: when a producer sends a message, when do we tell it "got it"? That's the acks setting:

  • acks=0 — fire and forget. The producer doesn't wait at all. Fastest, but a message can vanish if the leader dies before storing it.
  • acks=1 — wait until the leader has written it. Safe against most failures, but if the leader dies before any follower copied that message, it's lost.
  • acks=all — wait until every in-sync replica has the message. Now the message survives any single broker (indeed any failure short of losing the whole ISR). Most durable, highest latency, because you wait for the slowest replica in the ISR.

To keep consumers from ever reading a message that might still be lost, the broker exposes only up to the high-watermark: the highest offset that all ISR members have. Messages above the watermark exist on the leader but aren't yet visible to consumers. When the leader dies, a follower from the ISR is elected the new leader — and because every ISR member is at least at the high-watermark, no acknowledged, visible message is lost in the handover.

Interactive · replication & ISR Kill the leader; watch a follower take over and the ISR move
leader
B1
ISR size
3
acknowledged
0
last ack latency
⚠️
The durability illusion: acks=all only guarantees as much as the ISR is wide. If the ISR has shrunk to just the leader, "all" means "one." That's why production clusters also set min.insync.replicas ≥ 2 — refuse the write rather than acknowledge something that isn't actually replicated.

6Consumer groups & rebalancing

One consumer might not keep up with a busy topic. So consumers band into a consumer group: a team that shares the work of one topic by dividing its partitions among themselves. The rule is clean — each partition is assigned to exactly one consumer in the group at a time. Three consumers, twelve partitions: each gets four. The group as a whole reads every message once; no two members process the same partition.

The group's progress is stored as a committed offset per partition (Kafka keeps these in a special internal topic). When a consumer finishes processing up to offset 4,000 on its partition, it commits that number. If it crashes and another consumer takes over, the replacement resumes from the last committed offset — that's how the group remembers its place across restarts.

What happens when a consumer joins or leaves (a deploy, a crash, a scale-up)? The group runs a rebalance: partitions are revoked from everyone and re-divided so each surviving member gets a fair share. A group coordinator (a broker) orchestrates it; members briefly pause, get their new assignments, and resume. Rebalances are necessary but disruptive — during one, processing stalls — so newer protocols (cooperative/incremental rebalancing) try to move only the partitions that must move instead of stopping the world.

And the symmetry to remember: separate groups are fully independent. Group "billing" and group "analytics" each read the whole topic from their own offsets, oblivious to each other. That's the multi-reader replay power from Section 1, in operational form.

Interactive · consumer-group rebalance Add or remove consumers; watch the 6 partitions reassign live
consumers in group
2
partitions / consumer
3
total group lag
0

7Delivery semantics — what does "delivered" mean?

The hardest honesty in distributed systems: a message and an acknowledgement can both get lost, and the sender can't tell which happened. From that uncertainty come three possible guarantees, and you pick one by choosing when the consumer commits its offset.

  • At-most-once. Commit the offset before processing. If you crash mid-work, the offset already moved on, so that message is skipped forever. Fast, lossy. Fine for low-value telemetry.
  • At-least-once. Process first, commit the offset after. If you crash after processing but before committing, you'll re-read and reprocess that message on restart. Nothing is lost, but duplicates happen. This is the common default — and it demands your handler be idempotent (processing the same message twice does no extra harm).
  • Exactly-once. The holy grail: every message takes effect once, no loss, no duplicates. Kafka achieves it with two pieces — an idempotent producer (each message carries a sequence number so the broker silently drops retried duplicates) and transactions that bind the write of results and the commit of the consumed offset into one atomic step. Either both happen or neither does.

That's the deep idea worth carrying away: "exactly-once" is really "atomic append + offset-commit." If advancing your cursor and recording your output happen as one indivisible transaction, then a crash can never leave you having done one without the other — so there's nothing to duplicate and nothing to lose.

One more design choice underlies all of this: Kafka consumers pull. Rather than the broker pushing messages at consumers (and risking overwhelming a slow one), each consumer asks for the next batch when it's ready. Pull means the consumer sets its own pace — natural backpressure. The price is consumer lag: the gap between the log's tail and a consumer's offset. Watching lag is how you know whether your consumers are keeping up; growing lag means add capacity or the stream will outrun you.

λ
The FP framing: a consumer is a fold whose accumulator is (offset, sideEffects). At-least-once advances the offset after the effect; at-most-once before. Exactly-once makes the offset update and the effect a single atomic transaction — the functional move of "commit both or neither," lifted onto a stream.

8Coordination & metadata — who's in charge?

A cluster of brokers needs to agree on facts that change as machines come and go: which brokers are alive, which broker is the leader for each partition, what topics and partitions exist, and where a producer should send a given write. Someone has to be the source of truth for this cluster metadata, and crucially, everyone must agree on it even as nodes fail.

That's a consensus problem (Chapter 6 territory). Historically Kafka outsourced it to ZooKeeper — a separate, strongly consistent coordination service that stored membership and leadership and ran leader elections. It worked, but it meant operating a second distributed system alongside your first, with its own failure modes.

Modern Kafka folds coordination inward with KRaft (Kafka Raft). A small set of brokers act as controllers and run the Raft consensus protocol among themselves to maintain the cluster metadata as its own internal log — the same append-only primitive this whole chapter is built on, now storing "who leads partition 7" instead of user events. One controller is the active leader; it decides partition leadership and broadcasts the metadata to every broker. Fewer moving parts, faster failover, and a pleasing self-similarity: the log coordinates the logs.

🧭
A producer doesn't guess where to send a write. It first fetches metadata ("partition 3 of orders is led by broker B5") from any broker, then talks straight to the leader. When leadership moves, the producer's next metadata refresh redirects it — clients re-discover the topology rather than hard-coding it.

9Scale & ops — running it for real

The architecture is elegant; keeping it healthy is where the work lives. A few realities operators face:

  • Adding brokers / partitions. New brokers don't take load until partitions are reassigned onto them, which means copying potentially huge logs across the network — throttle it or you'll starve live traffic. You can add partitions to a topic, but it changes hash(key) % count, so existing keys may jump partitions and per-key ordering can break across the boundary. Plan partition count generously up front.
  • Hot-partition skew. A bad key (or one whale user) sends most traffic to a single partition. That partition's leader broker melts while others idle — the celebrity problem from Chapter 1, now at the partition level. Choose high-cardinality keys; sometimes salt the key for the hot entity.
  • Tiered storage. Keeping months of data on broker SSDs is expensive. Tiered storage offloads old, cold segments to cheap object storage (S3) while the broker still serves them transparently — letting you keep long retention (for replay) without paying for hot disk.
  • Monitoring. The two numbers that matter most: consumer lag (are readers keeping up?) and ISR shrinkage (is replication healthy?). A consumer group whose lag grows without bound will never catch up; an ISR that keeps dropping to 1 means your acks=all durability is quietly evaporating. Alert on both.

Here's the whole chapter as a checklist — the path from "a list you append to" to a production event backbone:

  1. 1Decouple with a log — producers append, consumers fold; messages stay after reading, enabling replay and many readers.
  2. 2Topic → partitions → offsets — the topic splits into append-only partitions; a consumer is just an offset per partition.
  3. 3Key for order, count for parallelism — same key → same partition (ordered); more partitions → more concurrent readers.
  4. 4Store as sequential segments — append-only segment files, page cache + zero-copy reads, retention by time/size or compaction.
  5. 5Replicate with leader/followers + ISR — pick acks for your durability/latency trade; the high-watermark hides unsafe messages.
  6. 6Share work in consumer groups — one partition per member, committed offsets track progress, rebalances redistribute on change.
  7. 7Choose delivery semantics — commit before (at-most), after (at-least + idempotent), or atomically (exactly-once).
  8. 8Coordinate metadata — ZooKeeper or KRaft tracks membership and partition leadership; clients fetch the topology.
  9. 9Operate it — watch lag and ISR, plan partitions, tame hot partitions, tier cold storage.

10The log + a cursor, as a tiny pure model

The whole chapter compresses into a few lines. A log is an immutable sequence you only append to. A consumer is a cursor — literally one integer offset — and reading is poll: hand back the message at the current offset (if any) and a cursor advanced by one. Nothing about the log knows the cursor exists; that's why a hundred cursors can read it independently. Flip the languages; the shape is identical.

λ
A consumer is just an offset into an immutable log — no state lives in the log itself, so any number of cursors read it without coordinating. And "exactly-once" is the bracketed version: make the append (or side-effect) and the offset-commit atomic and idempotent, so a crash can never split them. Append once, advance once — or neither.