Design Kafka from scratch

Not "Kafka has brokers, topics, partitions." Instead: two Principal Architects in a design review, where nothing is allowed to be a definition — every concept has to earn its place by solving a problem the previous decision created.

15 stages Problem → Constraint → Trade-off → Design Decision → New Problem
How this works: Architect A is extremely skeptical and challenges every assumption — never accepts a buzzword ("tight coupling," "high availability") without making you explain the actual mechanism. Architect B is you, designing from first principles. Read A's challenge, form your own answer, then reveal to compare against the reference dialogue.
A Interviewer — skeptical, never satisfied with a buzzword
B You — designing Kafka from zero, every choice must survive scrutiny
Stage 0

Don't say "Kafka"

Architect A

I don't want Kafka. I want a distributed event streaming platform. Convince me we even need one.

✎ think first

An e-commerce company generates orders, payments, inventory updates, notifications, analytics events, audit logs — and every service wants these events. How would you connect them? Form your own answer before revealing.

Reveal the dialoguereveal
B

Option 1: every service directly calls every other service.

Order
 |------ Payment
 |------ Analytics
 |------ Email
 |------ Inventory
 |------ Fraud
 |------ Recommendation
A

What's wrong with that? Don't say "tight coupling." Everyone says that. Explain.

B

Order service produces one event. Today, 5 consumers. Tomorrow, 50. Now Order service is suddenly responsible for retries, failures, back pressure, consumer availability, delivery guarantees. The producer has accidentally become a messaging platform — that violates single responsibility.

A

So producers shouldn't know consumers. Why?

B

Producers create events. Consumers decide what to do with events. Those responsibilities evolve independently. If the producer knows its consumers, every new consumer requires a producer deployment. That's architectural debt.

A

So introduce a broker. Why?

B

A broker separates time. Producer says "I've produced something." Consumer says "I'll process it whenever I'm ready." Neither waits for the other.

Producer --> [Broker] --> Consumer

Producer doesn't wait for consumer.
Consumer doesn't wait for producer.
Time coupling disappears.
A

Good. But you've only solved decoupling. Now convince me this isn't just RabbitMQ.

Where this leaves us: the broker is earned, not assumed — it exists to remove time coupling between producers and consumers. But "a broker" alone doesn't explain Kafka; it explains any message queue. Stage 1 is where the two start to diverge.
Stage 1

What are we optimizing?

Architect A

What is the primary optimization goal? Choose only one: latency, memory, CPU, storage, network, throughput, durability, consistency, availability. You cannot optimize everything.

✎ think first

Pick one. Then justify from hardware, not from a feature list, why that architecture naturally follows.

Reveal the dialoguereveal
B

Throughput. Not latency.

A

Why? Latency sounds more important.

B

At 1 million messages/sec, saving 2 milliseconds is meaningless if your architecture can't sustain the load in the first place. Kafka was designed for massive event pipelines, not request-response systems. Throughput dominates; latency becomes secondary.

A

If throughput is king, what architecture naturally gives maximum throughput? Don't say partitions. Think lower.

B

Sequential writes.

A

Why? Explain from hardware.

B
RANDOM WRITE:
  find location -> move disk head/pointer -> update metadata -> maybe touch indexes
  (expensive, unpredictable)

SEQUENTIAL WRITE:
  [existing data][NEW DATA APPENDED HERE]
  (just append — cheap, predictable)

Hard disks, SSDs, OS page cache, CPU caches — everything prefers sequential access. Modern SSDs are also far more efficient with sequential writes, due to lower write amplification and simpler internal management.

A

Then why does every database perform random writes?

B

Because databases optimize queries — "how do I find a record quickly?" Kafka optimizes appends — "how do I write the next record as fast as possible?" Two different goals.

Where this leaves us: throughput-first, sequential-write-native — this single choice is the seed of almost everything that follows: append-only storage, no in-place updates, and eventually, why Kafka looks nothing like a database internally even though people compare them constantly.
Stage 2

Why append-only?

Architect A

Suppose every message is stored forever: M1, M2, M3, M4, M5. Why not update messages instead of just appending?

✎ think first

What's fundamentally different about an "event" compared to a row in a database table?

Reveal the dialoguereveal
B

Events represent history. History should never change. "Order placed," "payment completed," "refund issued" — those are facts. Facts should be immutable.

A

Why is immutability important?

B

Because history becomes reproducible — analytics, auditing, reprocessing, disaster recovery, machine learning, all of it depends on replaying the exact same history. If history changes, truth changes.

A

So Kafka stores history, not state.

B
DATABASE: stores current truth
  Account balance: ₹40,000     <- one row, overwritten each update

KAFKA: stores how truth evolved
  [Deposit ₹10,000][Withdraw ₹5,000][Deposit ₹35,000]     <- the whole story, never overwritten

Exactly. Databases store current truth. Kafka stores how truth evolved.

Where this leaves us: once you accept "history, not state," append-only isn't a storage optimization anymore — it's the only model consistent with what an event actually is. This is also the moment "database vs Kafka" stops being a fair comparison; they're answering different questions.
Engineering Philosophy

Events are facts about what happened. Facts don't get updated. If you design a system that needs to "correct" an event, you're not modeling events — you're modeling state, and you need a different tool.

Stage 3

Why don't consumers delete messages?

Architect A

Consumer finishes reading — delete the message. Simple. Why doesn't Kafka do that?

✎ think first

If deleting-after-read seems obviously wrong, name the exact scenario that breaks it.

Reveal the dialoguereveal
B

Question back: which consumer? Analytics? Audit? Fraud? Backup? A new consumer joining tomorrow?

Event: "Order Placed"
        |
   -----+------+------+---------+
   |     |      |      |        |
Analytics Audit Fraud Backup  (new consumer, joins next month)

Different consumers move at different speeds. Deleting after the first read destroys history for everyone else.

A

So who owns progress?

B

The consumer. Not the broker. Broker owns data. Consumer owns position. That's why offsets belong to consumers, not to Kafka.

A

That decision changes everything.

Where this leaves us: this single reframing — "position is the consumer's problem, not the broker's" — is what makes replay, multiple independent consumer groups, and late-joining consumers all fall out for free, instead of needing to be bolted on as separate features.
Stage 4

The real scaling problem

Architect A

One log. One machine. Can we scale forever?

✎ think first

What exactly does "partitions" solve — and what's the hidden cost of having too many of them?

Reveal the dialoguereveal
B

No. Eventually CPU, disk, NIC, memory all saturate. We need horizontal scaling.

A

Most people immediately say "partitions." I don't want that answer. Tell me what problem partitions actually solve.

B
BEFORE:                        AFTER:
  1 log                          100 logs
  1 writer                       100 writers

Concurrency appears naturally — each log is independent.
A

Now challenge yourself. If partitions increase parallelism, why not create 100,000 partitions?

B

Because partitions aren't free. Every partition carries:

per partition:
  - metadata
  - leader election state
  - replication state
  - network connections
  - open files
  - page cache usage
  - memory overhead

100,000 partitions -> 100,000x this overhead
Eventually coordination cost EXCEEDS throughput gain.
A

Exactly. Architecture is always balancing one bottleneck against another.

Where this leaves us: partitions aren't a free scaling dial — they're a trade of coordination overhead for parallelism. Every later stage that touches partition count (sizing, rebalancing, controller load) is really just this same trade-off showing up again in a new place.
Engineering Philosophy

More partitions means more parallelism — up to a point. Beyond that point, you're paying coordination cost for throughput you'll never see. Picking the right partition count is a capacity decision, not a "bigger is better" default.

Stage 5

Ordering

Architect A

The CEO says "I want perfect ordering." Can you give it?

✎ think first

Can you have both perfect global ordering and massive parallel throughput? If not, why not — in one sentence?

Reveal the dialoguereveal
B

Yes — one partition.

A

Can you still do 5 million messages/sec?

B

No. One ordered stream means one serialization point.

GLOBAL ORDER:               MASSIVE PARALLELISM:
  [1 log][1 writer]           [log][log][log]...[log]
  perfectly ordered           100 independent writers
  throughput capped              no single global order

You get ONE of these. Not both.
A

Exactly. That's the first unavoidable trade-off in Kafka. You don't "implement" your way around it — you choose one side.

Where this leaves us: Kafka's actual answer is neither extreme — order within a partition, parallelism across partitions — but that compromise only makes sense once you've accepted there's no design that avoids this trade-off entirely.
Stage 6

Replication

Architect A

Why replicate? Don't say "high availability." Everyone says that.

✎ think first

If three replicas exist, who should be allowed to accept writes — all three, or one? Justify it.

Reveal the dialoguereveal
B

Replication exists because hardware eventually fails. The real question isn't if a broker dies — it's when. The architecture has to treat failure as normal, not exceptional.

A

Good. Three replicas — who should accept writes? All three?

B
ALL THREE ACCEPT WRITES:
  Replica 1: [A][B]         <- network partition happens
  Replica 2: [A][C]         <- diverged history!
  Replica 3: [A][B]

  Which one is "correct"? No good answer once histories diverge.

ONE LEADER ACCEPTS WRITES:
  Leader:     [A][B][C]  <- one authoritative order
  Follower 1: [A][B][C]  <- copies leader's order
  Follower 2: [A][B][C]  <- copies leader's order

If multiple replicas independently accept writes, they can diverge during a network partition — reconciling conflicting histories while preserving one ordered log becomes extremely hard. A single leader gives one authoritative write order; followers just replicate it.

A

What did we sacrifice?

B

Write availability during leader transitions. We traded some availability for a consistent, ordered log.

Where this leaves us: "one leader" isn't a Kafka quirk — it's the direct, unavoidable consequence of refusing to let history diverge. Stage 9 (leader election) is where we find out what happens the moment that one leader actually dies.
Stage 7

The hardest question: duplicates

Architect A

Producer sends a message. Leader writes it. Leader crashes. Producer times out. Producer retries. Did we create a duplicate?

✎ think first

If the answer is "possibly," what's the minimum piece of information the broker needs to tell a retry apart from a genuinely new message?

Reveal the dialoguereveal
B

Possibly.

A

How do we fix it?

B

The broker must recognize that a retry is the same logical send, not a new one.

Producer sends "Order Created" -----> Leader writes it -----> *** crashes before ACK ***
Producer times out -----> retries -----> "is this new, or the same one again?"

Fix: give the producer an IDENTITY + a SEQUENCE NUMBER
  producer_id=100, sequence=55  --> broker remembers this pair
  retry arrives with the SAME (100,55) --> broker recognizes it, discards the duplicate
A

Now you've entered the realm of exactly-once semantics. Notice how we arrived there naturally, from failure analysis — not because it was a feature on a slide.

Where this leaves us: this is the pattern across the whole conversation — every "feature" (offsets, leaders, sequence numbers) showed up because a previous decision created a problem that needed solving, never because it was on a spec sheet.
Engineering Philosophy

At-least-once delivery is what you get by default when retries exist. Exactly-once requires the consumer to be idempotent. You don't get to skip that design step — you just choose when to do it.

Stage 8

Why does ISR exist?

Architect A

You have a leader and two followers. One follower is on a slow disk and consistently lags. Should the leader wait for it before acking a producer with acks=all?

✎ think first

If "wait for everyone" is too slow and "wait for no one" is too risky, what's the actual rule that should decide who counts?

Reveal the dialoguereveal
B

No — not forever. If the leader always waits for every replica, one permanently slow disk caps the throughput of the entire partition for everyone.

A

So drop the requirement to replicate to it at all?

B

No — that's the other extreme, and it silently weakens durability. The real answer is a dynamic set: replicas that are currently caught up count; the ones that fall behind temporarily stop counting, without being abandoned.

Leader:     offset 105
Follower A: offset 105   <- caught up
Follower B: offset 60    <- far behind (slow disk)

ISR (In-Sync Replicas) = { Leader, Follower A }
  acks=all only waits for THESE two, not for B

Follower B keeps replicating in the background.
Once it catches up -> rejoins the ISR -> counts again.
A

So ISR isn't a fixed replication factor — it's a moving target?

B

Exactly. Replication factor is "how many copies exist, eventually." ISR is "how many of those copies can I actually trust right now to define durability." Those are two different numbers, and conflating them is a common mistake.

Where this leaves us: ISR is what happens when you refuse both extremes — "wait for everyone" (too slow) and "wait for no one" (too risky) — and instead make the durability contract track reality instead of a static config number.
Stage 9

Leader election under failure

Architect A

The leader dies. Follower A was in the ISR. Follower B was not — it was lagging. Who becomes the new leader?

✎ think first

What breaks if you let the lagging follower become leader? What breaks if you refuse to?

Reveal the dialoguereveal
B

Follower A — the one that was actually in the ISR. Electing B risks losing every message B never replicated.

Leader (dead):  [100][101][102][103][104][105]
Follower A (ISR):        [100][101][102][103][104][105]   <- fully caught up
Follower B (lagging):    [100][101][102]                    <- missing 103,104,105

Elect A --> no data lost, those messages survive
Elect B --> messages 103,104,105 are GONE, silently
A

So you'd never elect B, under any circumstances?

B

That's the "clean" vs "unclean" leader election choice. Clean election refuses to elect anyone outside the ISR — if no ISR member is available, the partition simply goes offline until one comes back. Unclean election allows electing an out-of-ISR replica to restore availability, accepting silent data loss.

A

Which one does a payments system choose?

B

Clean, always. An offline partition is a visible, debuggable outage. Silently losing transactions is a hidden one — and hidden failures are far more dangerous in a financial system than visible ones.

Where this leaves us: this is availability vs. durability again, at a different layer than stage 6 — and it's a config choice (unclean.leader.election.enable), not a fixed architectural answer, precisely because different topics can reasonably choose differently.
Stage 10

Producer acknowledgements, revisited

Architect A

You've now built ISR and leader election. Given that, why does Kafka expose three different acks settings instead of just always waiting for the full ISR?

✎ think first

Name one real workload where waiting for the full ISR would be actively the wrong choice.

Reveal the dialoguereveal
B

Because not every message is worth the latency cost of waiting for the ISR. A UI click-tracking event and a money-transfer event have completely different loss tolerances.

acks=0:   producer fires and forgets, doesn't even check    (fastest, riskiest)
acks=1:   leader writes locally, acks immediately             (leader-durable only)
acks=all: leader waits for the full ISR before acking          (slowest, safest)

click-tracking event  --> acks=0 or 1, losing one is fine
money-transfer event  --> acks=all, non-negotiable
A

So this isn't a broker setting — it's a producer decision, made per message?

B

Per producer, typically scoped per topic. The broker enforces a floor (min.insync.replicas) — the producer chooses how hard to push against that floor. Neither side alone decides the durability contract; it's negotiated.

Where this leaves us: this is the same "not one NFR for the whole system" idea from requirements-gathering, now showing up as a literal wire-protocol parameter — proof that a good early design decision keeps paying off many stages later.
Stage 11

Consumer group rebalancing

Architect A

Multiple consumers read the same topic in parallel, each owning a subset of partitions. A consumer crashes. Who decides how to reassign its partitions — and why does that question even need a dedicated answer?

✎ think first

What goes wrong if the surviving consumers negotiate the reassignment among themselves, with no single decision-maker?

Reveal the dialoguereveal
B
Consumer A: "I think I own partition 2 now"
Consumer C: "No, I already claimed partition 2"
Consumer A: "Are you sure?"

Two consumers reading the same partition -- double processing.
Or: nobody claims it -- partition goes unread.

Without one decision-maker, you get exactly this — a distributed agreement problem, solved badly, ad hoc, by the consumers themselves.

A

So centralize it. Doesn't that become a bottleneck?

B

Only if it sat on the hot path — and it doesn't. It only acts on join, leave, or crash. Those are rare events. The actual message flow (produce, fetch) never touches this coordinator at all.

Hot path (constant):     Producer --> Broker --> Consumer
Rare path (occasional):  Consumer join/leave/crash --> Coordinator reassigns
A

What's the actual risk, then?

B

Not centralization — a rebalance storm: if consumers join and leave faster than assignment can stabilize, the group spends more time reassigning than actually reading. That's a tuning problem (session timeouts, static membership), not a reason to abandon centralization.

Where this leaves us: the same shape as stage 6's leader — centralize the decision that's rare and needs one answer, keep it off the path that's constant and needs to be fast.
Stage 12

Backpressure: pull vs. push

Architect A

Why does the consumer ask the broker for messages, instead of the broker pushing messages to the consumer as they arrive?

✎ think first

What happens to a slow consumer under a push model that can't happen under a pull model?

Reveal the dialoguereveal
B
PUSH MODEL:
  Broker --> fires messages at consumer as fast as they arrive
  Slow consumer --> messages pile up somewhere (broker buffer? consumer memory?)
  Broker now has to guess every consumer's capacity, per consumer

PULL MODEL:
  Consumer --> "give me the next batch, when I'm ready"
  Broker --> just serves what's asked, never guesses
  Slow consumer simply pulls less often -- no special handling needed

Under push, the broker has to actively manage backpressure for every consumer — a genuinely hard, per-consumer problem. Under pull, backpressure is just... the natural consequence of the consumer asking less often. It's not solved; it's avoided.

A

Doesn't pull mean higher latency — the consumer has to keep asking?

B

Only if it's a naive poll-in-a-tight-loop. Kafka's fetch is a long-poll — the broker holds the request open until either data arrives or a timeout hits, so pull doesn't have to mean "ask constantly." You get pull's simplicity without paying the naive-polling latency tax.

Where this leaves us: pull isn't a minor API style choice — it's what lets the broker stay simple and dumb about per-consumer speed, consistent with the whole "consumer owns its own progress" decision from stage 3.
Stage 13

Storage internals: why is this actually fast?

Architect A

You've said "sequential writes" three times now. That explains fast writes. What makes reads fast — a consumer fetching data that was written five minutes ago?

✎ think first

Where does data actually live between "written to disk" and "sent over the network to a consumer" — and how many times does it get copied along the way?

Reveal the dialoguereveal
B

Two things: the OS page cache, and zero-copy transfer.

NAIVE read path (multiple copies):
  Disk -> kernel buffer -> application memory -> socket buffer -> NIC
         (copy 1)          (copy 2)              (copy 3)

Every copy costs CPU cycles and memory bandwidth -- for data
the application never actually needed to touch or transform.
A

So avoid the copies?

B
ZERO-COPY read path (sendfile-style):
  Disk -> kernel buffer -----------------------> NIC
         (data never enters application memory at all)

Kafka doesn't deserialize, transform, or touch the bytes on the read path — it just moves them. That means the OS can transfer straight from the page cache to the network socket, skipping the application-memory hop entirely.

A

And the page cache — why does that matter separately from zero-copy?

B

Recently-written segments are still sitting in the OS's page cache in RAM — most "reads" for recent data never actually touch the physical disk at all. Combined with zero-copy, a consumer reading data from a few seconds ago is close to a pure memory-to-network operation, not a disk operation.

Where this leaves us: "just append to disk" from stage 1 was only half the story — the other half is refusing to touch the bytes on the way back out, which is exactly what makes both directions of the pipe fast for the same underlying reason: doing less work per byte.
Stage 14

Kafka 2.0 — what would you redesign?

Architect A

Last question. If you were starting today, with everything you now know, what's the one decision from this conversation you'd revisit?

✎ this one has no reference answer

This is deliberately open. Pick one stage's decision — ordering, ISR, leader election, pull vs. push, anything — and argue what you'd change and what you'd give up by changing it. A real answer defends a trade-off; it doesn't just criticize.

No answer key. Notice what this whole conversation never did: it never said "Kafka has brokers," "Kafka has topics," "Kafka has partitions" as a starting fact. Every concept showed up because a previous decision became insufficient —
Problem → Constraint → Trade-off → Design Decision → New Problem → Next Design Decision
— not Feature → Definition → Next Feature. That's the actual difference between memorizing Kafka and understanding it. Your answer to this last question is the real test of which one you did.
Stages not covered here — extend this same format into them yourself:
  1. Cross-rack / cross-region replication
  2. Performance tuning under real production load
  3. Exactly-once semantics end-to-end (producer + consumer transactions)
  4. Log compaction as a design choice, not just a feature
  5. Controller quorum design (KRaft) from first principles
  6. Schema evolution as an organizational, not technical, problem