Search Tech Journey

Find topics, journeys and posts

6-month learning plan48 / 130
back to blog
data engineeringintermediate 55m read

S048 · Kafka — Topics, Partitions, Consumer Groups

The distributed commit log that eats the world. Learn what Kafka actually is (a durable log, not a queue), why partitions are the unit of parallelism, and how consumer groups let a thousand consumers share the load without losing a message.

🗄️Data EngineeringM05 · Data Engineering· Session 048 of 130 90 min

🎯 Model a real problem as Kafka topics + partitions + consumer groups, and reason about ordering, throughput, and rebalance cost like an owner, not a user.

Why this session exists

Every "event-driven" system, every streaming pipeline, every "let's decouple with a queue" architecture built after 2015 is really "we picked Kafka." But Kafka is not a queue, and treating it as one produces a beautiful cascade of production bugs: messages you thought were consumed but weren't, ordering guarantees that don't exist, partition counts that lock you into a limit forever. Ninety minutes here will save you six months of on-call.

You will be able to
  • Explain a topic, a partition, an offset, and a consumer group to a junior in one paragraph each.
  • Predict message ordering guarantees given a key and partition count.
  • Choose a partition count that doesn't screw you 2 years from now.
  • Read a consumer-group lag chart and know when to panic.
  • Reason about the ‘exactly-once’ contract end-to-end (producer + broker + consumer).

Prerequisites

  • S044 — NoSQL landscape (Kafka is the ‘log’ family).
  • S046 — Batch vs streaming (Kafka is the streaming substrate).


(a) Intuition · 5 min

A shared, immutable diary
🌍 Real world

Imagine a village of 100 people who all keep a single shared diary. Anyone can write a new page at the end (never edit or delete). Anyone can start reading from any past page. Multiple readers can read at their own pace and remember their own bookmark. That's Kafka: a distributed, append-only, replayable log.

Now imagine the village is huge, so you split the diary into 12 separate diaries kept in different houses (partitions). Messages about crops go to diary 3, messages about weather go to diary 7 — deterministic hash by topic. Any reader can subscribe to some or all diaries.

💻 Code world

A topic is a category of messages ("orders", "clicks", "audit_events"). Every topic is split into partitions — the unit of parallelism, ordering, and storage. Each partition is a sequential file on disk, replicated to N brokers. Producers append; consumers track their offset (bookmark).

Multiple consumers form a consumer group. Kafka assigns each partition to exactly one consumer in the group. Add consumers → more parallelism (up to partition count). Kill a consumer → its partitions rebalance to survivors.

Six ideas that unlock everything

The Kafka vocabulary you need
  • Topic — a category of messages (e.g. ‘orders’). Logical grouping.
  • Partition — the unit of parallelism, ordering, storage. A topic has N partitions.
  • Offset — a monotonically increasing integer per partition. The consumer's bookmark.
  • Producer — appends to (topic, key). Key hashes to a partition; same key → same partition → same order.
  • Consumer group — set of consumers sharing work. Kafka assigns each partition to exactly one consumer in the group.
  • Replication factor — how many brokers keep a copy. RF=3 is the standard for prod.

A quick history so you know why the world looks like this

  1. 2010
    Kafka at LinkedIn
    Jay Kreps, Neha Narkhede, Jun Rao build it to unify LinkedIn's activity streams and operational metrics.
  2. 2011
    Open-sourced
    Apache incubation. ‘Kafka’ is Kreps's favourite author.
  3. 2013
    Confluent founded
    Same team commercialises Kafka. Ships Schema Registry, Kafka Connect.
  4. 2017
    Exactly-once semantics ship
    Idempotent producer + transactions. Multi-year effort.
  5. 2019
    KIP-500 announced — remove ZooKeeper
    Self-managed metadata via KRaft. Simpler ops.
  6. 2023
    Kafka 3.5 · KRaft GA
    ZooKeeper is optional. Deployments halve in size.

(b) Visual walkthrough · 15 min

Kafka cluster anatomy

Producer → partition → consumer flow

Partition count — the one-way door

Too few partitions

Bottleneck on parallelism.

  • Max consumers in group = partition count
  • One consumer per partition; extras idle
  • Bump = new partitions, but old data doesn't rebalance to them
  • Existing keys keep going to old partitions
Too many partitions

Overhead everywhere.

  • More open file descriptors per broker
  • More metadata in the controller
  • Slower controller election, longer failover
  • Confluent recommends \<4000 partitions per broker
Sweet spot

Room to grow ×2-3, not ×100.

  • Target: peak consumer count × 2
  • Common: 12, 24, 50, 100 for medium topics
  • Multiples of typical replication factor
  • Revisit yearly as scale grows

Consumer group rebalance

1
Consumer joins / leaves

Heartbeats stop, group coordinator notices.

2
Stop-the-world rebalance

All consumers pause; coordinator reassigns partitions.

3
Fetch resumes from committed offset

Consumers resume from where the previous owner committed.

4
Duplicates possible

If the previous owner processed but didn't commit, the new owner re-processes. Sink must be idempotent.

5
Cooperative rebalance (Kafka 2.4+)

Only affected partitions pause. Standard in Streams / Connect. Enable for lower disruption.


Common misconception
✗ What most people think

"Kafka is a message queue. Consumers read messages and the messages are removed, like RabbitMQ or SQS."

✓ What is actually true

Kafka is a distributed, replicated, append-only log. Reading does not remove anything — messages are retained by time or size policy regardless of consumption. A consumer is just a cursor (an offset) into an immutable sequence. That is why many independent consumer groups can read the same topic, and why you can rewind and reprocess history.

Why the myth is so sticky

Because the API looks like a queue — produce, consume, acknowledge — and the words "topic" and "consumer" come straight from messaging systems. The model breaks the first time you need to add a second consumer of an existing topic, or replay last week's events after fixing a bug. In a real queue, both are impossible: the messages are gone. In Kafka both are trivial, and that single difference is why event-driven architectures are built on it.

Prove it to yourself

The offset is the whole abstraction — reset it and history replays:

# Two independent groups reading the SAME topic, each with its own cursor.
# Neither affects the other, and neither removes data.
kafka-console-consumer --topic orders --group billing    --from-beginning
kafka-console-consumer --topic orders --group analytics  --from-beginning

# Replay after fixing a bug - impossible in a real queue:
kafka-consumer-groups --group analytics --topic orders \
  --reset-offsets --to-datetime 2024-01-01T00:00:00.000 --execute

# Data is removed by RETENTION POLICY, never by consumption:
#   retention.ms      - drop segments older than N
#   retention.bytes   - cap partition size
#   cleanup.policy=compact - keep the latest value per key, forever
From first principles
Start with the question

Why does Kafka guarantee ordering only within a partition, and never across a topic? A single global order sounds strictly better — why refuse to provide it?

  1. 1
    A total order over all messages in a topic requires a single point that assigns sequence numbers.
    forced by · two independent assigners cannot agree on relative order without coordinating on every message
  2. 2
    A single sequencer caps throughput at what one machine can do, and makes that machine a single point of failure.
    forced by · every message must pass through it, so it cannot be scaled horizontally by definition
  3. 3
    Kafka's central goal is horizontal scalability, so it splits a topic into partitions, each an independent log with its own leader broker.
    forced by · independent logs on different machines is the only way throughput scales with broker count
  4. 4
    Two messages written to different partitions therefore have no defined relative order — no shared clock and no coordination exists between their leaders.
    forced by · ordering across independent logs would require exactly the coordination that was removed to gain scale
  5. 5
    But ordering is only needed between causally related events — two updates to the same account, two events for the same user — and those can be forced into the same partition by using a partition key.
    forced by · partition assignment is a deterministic hash of the key, so equal keys always land in the same partition
⇒ Therefore

Therefore per-partition ordering plus key-based partitioning gives you exactly the ordering guarantee you need, at a cost of exactly the coordination you can afford. The key choice is the ordering choice.

And note what this predicts: consumer parallelism within a group is capped at the partition count, because a partition is assigned to at most one consumer in a group — that is the only way per-partition order can be preserved on the read side too. So adding consumers beyond the partition count does nothing, and repartitioning a live topic changes which partition a key hashes to, breaking ordering for in-flight keys. Both of those follow directly from the derivation, and both surprise people in production.

Mental modelA shared, replayable log with cursors

Picture one enormous append-only ledger, sharded into partitions so it can be written by many machines at once. Producers only ever append. The ledger keeps entries according to a retention policy, entirely independent of who has read them.

Consumers are bookmarks. Each consumer group holds its own bookmark per partition and moves it forward as it processes. Nothing is consumed in the destructive sense — reading is just advancing a number. Adding a new consumer means starting a new bookmark, possibly at the very beginning.

  • Ordering is per-partition only. If two events must be ordered, they must share a partition key.
  • Max useful consumers per group = partition count. Partition count is a capacity decision made up front and painful to change.
  • Delivery is at-least-once by default. Exactly-once effects come from idempotent writes or transactional produce-consume, never from delivery guarantees alone.
  • Log compaction (cleanup.policy=compact) retains the latest value per key indefinitely, turning a topic into a durable changelog you can rebuild state from — the basis of CDC and stream-table duality.
🔔 Fires when you see

Fire this model the moment you see: several systems needing the same events · a need to reprocess history after a bug fix · decoupling producers from consumers · change data capture from a database · a consumer group lagging · a hot partition · "why did these two events arrive out of order?" · anywhere you were about to have service A call service B synchronously for something that is really a notification.

The tradeoff

You must choose a partition key for a high-volume topic. Key by entity ID, key by nothing (round-robin), or key by a composite?

Key by entity ID (user, account, device)
+ you gain all events for one entity land in one partition, so per-entity ordering is guaranteed and stateful consumers can keep that entity's state locally with no coordination
− you pay a heavy entity creates a hot partition that no amount of scaling fixes — one partition has one leader and one consumer, so its throughput ceiling is one machine
pick when per-entity ordering is a correctness requirement and the entity distribution is reasonably flat — the common and usually correct default
No key (round-robin)
+ you gain perfectly even distribution across partitions, maximum throughput, no hot partitions possible, and producer batching is most efficient
− you pay no ordering guarantee whatsoever, so any consumer that needs ordered-per-entity processing must reorder itself — which usually means buffering, which usually means it cannot
pick when events are genuinely independent — logs, metrics, telemetry — where each record is processed in isolation
Composite or salted key
+ you gain splits a hot entity across several partitions while keeping related events grouped, recovering throughput without going fully unordered
− you pay ordering now holds only within the salted sub-key, so consumers must handle events for one entity arriving on several partitions; and the salt is extra logic on both sides that must stay in sync
pick when a measured hot key is capping throughput and you can define a sub-grouping (entity + hour, entity + region) whose internal ordering is what you actually need
What a senior engineer actually does

Key by the entity whose ordering matters, and check the distribution before committing — a skewed key is the Kafka equivalent of a skewed Spark join, and it has the same signature: one worker saturated while the rest idle. The partition count deserves the same care, because it is far easier to over-provision partitions on day one than to repartition a live topic later, and repartitioning changes key placement and breaks ordering for keys in flight.

The deeper point worth carrying: the partition key encodes your consistency boundary. Everything sharing a key can be processed with local state and guaranteed order; anything crossing keys requires coordination you will have to build yourself. Choosing the key is therefore an architectural decision about where transactions can exist, not a configuration detail — which is exactly the same decision as choosing a shard key in a distributed database.


(c) Hands-on · 25 min

Spin up single-node Kafka in Docker, produce/consume with two consumer groups, watch a rebalance. Save as kafka_demo.sh.

#!/usr/bin/env bash# kafka_demo.sh one-node Kafka in Docker, producer + two consumer groups + rebalance.set -euo pipefail log() { printf "\033[1;36m %s\033[0m\n" "$*"; }cleanup() { docker rm -f kafka-demo >/dev/null 2>&1 || true; }trap cleanup EXIT log "1/5 Start Kafka in KRaft mode"docker run -d --rm --name kafka-demo \ -p 9092:9092

What each block does

Anatomy of the script

KRaft mode (no ZooKeeper)
Modern default — Kafka manages its own metadata. Simpler ops, single binary.
config
6 partitions on a 1-broker demo
Even on one broker you get 6-way parallelism — each partition is an independent file.
parallelism
parse.key=true, key.separator=:
The producer console tool splits ‘key:value’. Real producers pass key/value as separate args in the client library.
producer
Same customer_id → same partition
murmur2 hash of key mod 6. Ordering guaranteed within the customer's stream.
keying
Two consumer groups
Each maintains its own offsets. Orders can be processed AND analysed independently — the classic ‘decouple with Kafka’ win.
groups
kafka-consumer-groups --describe
Shows current-offset vs log-end-offset per partition. Lag = end - current. Monitor this in prod.
ops
Try itWatch a consumer-group rebalance

Open two terminals, run in each:

docker exec -it kafka-demo /opt/kafka/bin/kafka-console-consumer.sh \
  --bootstrap-server localhost:9092 --topic orders --group demo --from-beginning

Both consumers share the load — 3 partitions each. In a third terminal, watch:

watch -n 1 "docker exec kafka-demo /opt/kafka/bin/kafka-consumer-groups.sh \
  --bootstrap-server localhost:9092 --describe --group demo"

Kill one consumer with Ctrl-C. Within ~10 seconds the survivor's partition set jumps from 3 to 6 — that's the rebalance.

💡 Hint · Run two consumers in the same group in two terminals; kill one, watch the other's partition set expand from 3 to 6 via `--describe`.

(d) Production reality · 15 min

War story LinkedIn7+ trillion messages/day across all Kafka clusters
🔥 What broke

Early LinkedIn Kafka clusters had huge topics with thousands of partitions each. Controller failover took 5+ minutes because it had to re-elect leaders for every partition serially. During failover, the whole cluster was effectively unavailable.

🧯 The fix

Sharded topics across clusters and adopted KIP-500 (KRaft). Controller now handles metadata via a Raft consensus quorum rather than ZooKeeper reads/writes. Failover dropped to \\<10 seconds even with 100k+ partitions.

🎓 Lesson to steal
Partition count is a scale-limiting hyperparameter. Plan for the metadata cost, not just the storage cost. Stay under a few thousand per broker.
Post-mortem
War story Slack· 2018major outage post-mortem
🔥 What broke

Slack's job-queue Kafka cluster had 6 000 partitions on a small cluster. A single-broker failure triggered a rebalance storm — thousands of consumer groups reassigning simultaneously, each pause blocking downstream systems. The cascade took the messaging pipeline down for hours.

🧯 The fix

Split into multiple smaller clusters by workload class. Adopted cooperative rebalance protocol (Kafka 2.4+) so only affected partitions pause. Reduced blast radius of any single broker failure by 10×.

🎓 Lesson to steal
One Kafka cluster shared across every team = one broker failure impacts everyone. Split by criticality tier: mission-critical, standard, best-effort.
War story Common failure — silent consumer offset commitevery team, eventually
🔥 What broke

A consumer processes a message, writes it to a downstream sink (Postgres row insert), then crashes before committing the offset. On restart, the consumer re-processes the message and inserts a duplicate. Later analytics show inflated numbers no one can explain.

🧯 The fix

Two options: (a) make the sink idempotent (natural key or dedup step), or (b) use Kafka transactions + read_committed consumer for exactly-once semantics. Most teams choose (a) because (b) has real throughput cost.

🎓 Lesson to steal
‘At-least-once + idempotent sink = effectively exactly-once, cheaper than transactions’. This pattern is more important than the exactly-once feature.

Where this shows up in the rest of the plan

Kafka is the log everything else consumes
S049 · Stream processing
Flink/Spark Streaming read from Kafka topics.
S050 · Airflow
Sensors on Kafka topics; DAGs triggered by events.
S055 · CDC (Debezium)
Converts a Postgres WAL into a Kafka topic.
S086 · Sagas
Distributed workflow steps as Kafka events.
S110 · Caching + invalidation
Cache-invalidation events flow via Kafka.
S121 · System design — chat
Every real chat backend uses Kafka for message fanout.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

  1. Kafka in one sentence — log, not queue.
  2. Partition = unit of what? — parallelism, ordering, storage.
  3. When is order guaranteed — per partition, so key by the entity that needs order.

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.