Apache Kafka Topics, Partitions and Offsets: How the Log Works
System Design & ArchitectureLesson 1 of 46 min read
What you'll learn
- Describe a Kafka topic as a set of partitioned, append-only logs
- Explain what an offset is and why it is only meaningful inside one partition
- Predict which partition a keyed record lands in and what that implies for ordering
- Create and describe a topic with the standard command-line tools
On this page
Six events from a network of parcel-locker kiosks are written to a topic that has three partitions. Before any explanation, look at where each one ended up, using a tiny Python model of the storage layer.
import zlib
class Topic:
def __init__(self, name, partitions):
self.name = name
self.parts = [[] for _ in range(partitions)]
def append(self, key, value):
# Real Kafka uses murmur2 on the key bytes; crc32 stands in here.
p = zlib.crc32(key.encode()) % len(self.parts)
self.parts[p].append((key, value))
return p, len(self.parts[p]) - 1 # (partition, offset)
def read(self, partition, offset, max_records=10):
return self.parts[partition][offset:offset + max_records]
orders = Topic("parcel-events", partitions=3)
events = [
("locker-7", "PARCEL_DROPPED"),
("locker-2", "PARCEL_DROPPED"),
("locker-7", "CODE_SENT"),
("locker-9", "PARCEL_DROPPED"),
("locker-7", "PARCEL_COLLECTED"),
("locker-2", "DOOR_JAMMED"),
]
for key, value in events:
p, off = orders.append(key, value)
print(f"{key:9} {value:17} -> partition {p}, offset {off}")
print()
for p in range(3):
print(f"partition {p}: {[v for _, v in orders.parts[p]]}")
print()
# Two independent readers keep their own position; the log is not consumed.
analytics_pos = 0
billing_pos = 0
batch = orders.read(0, analytics_pos)
analytics_pos += len(batch)
print("analytics read", len(batch), "records from p0, next offset", analytics_pos)
batch = orders.read(0, billing_pos, max_records=1)
billing_pos += len(batch)
print("billing read", len(batch), "record from p0, next offset", billing_pos)
print("p0 still holds", len(orders.parts[0]), "records")Output
locker-7 PARCEL_DROPPED -> partition 0, offset 0 locker-2 PARCEL_DROPPED -> partition 0, offset 1 locker-7 CODE_SENT -> partition 0, offset 2 locker-9 PARCEL_DROPPED -> partition 1, offset 0 locker-7 PARCEL_COLLECTED -> partition 0, offset 3 locker-2 DOOR_JAMMED -> partition 0, offset 4 partition 0: ['PARCEL_DROPPED', 'PARCEL_DROPPED', 'CODE_SENT', 'PARCEL_COLLECTED', 'DOOR_JAMMED'] partition 1: ['PARCEL_DROPPED'] partition 2: [] analytics read 5 records from p0, next offset 5 billing read 1 record from p0, next offset 1 p0 still holds 5 records
Three things in that output are the whole foundation of Kafka. Every record for locker-7 went to partition 0, in the order it was written. Offsets restart from 0 in each partition, so offset 0 exists in partition 0 and again in partition 1. And reading did not remove anything: two readers kept their own positions and the partition still holds all five records afterwards.
A log, not a queue
A traditional queue hands a message to one worker and deletes it once acknowledged. Kafka does something different. A partition is an append-only file (physically a series of segment files on the broker's disk). Producers only ever add to the end. Each record gets the next sequential number in that partition, its offset. Consumers do not take records out; they remember a position and read forward from it.
That one design decision explains most of Kafka's behaviour. Because reading is non-destructive, many independent applications can read the same data at their own pace: the analytics job, the billing job and a fraud checker above are three readers of one log. Because a reader's position is just a number, a crashed reader can resume exactly where it stopped, and a new reader can start from the beginning to rebuild its state. Records disappear only when the retention policy removes old segments, which you will configure in the last lesson.
The vocabulary, with what is unique where
| Term | What it is | Unique within |
|---|---|---|
| Topic | A named stream of records, such as parcel-events | The cluster |
| Partition | One ordered log that belongs to a topic; the unit of parallelism and of ordering | The topic |
| Offset | Sequential position of a record in its partition, starting at 0 | The partition only |
| Key | Optional bytes you attach to a record; used to choose the partition | Not unique at all |
| Replica | A copy of a partition kept on another broker for fault tolerance | The partition |
| Broker | One Kafka server process that stores partitions and serves clients | The cluster |
The practical consequence: a record's true address is the triple (topic, partition, offset). Saying "offset 41" without a partition is like saying "seat 41" without a row. This matters later when you need to deduplicate or report a poison message.
Keys decide partitions, and partitions decide ordering
Kafka guarantees order within a partition and nothing across partitions. So the question "which records must stay in order?" becomes "which records must share a key?". If every event about one locker carries the locker id as its key, they all hash to the same partition and are read back in write order. Events about different lockers may interleave arbitrarily, and that is usually fine.
Notice that the demo is lopsided: partition 0 got five of six records and partition 2 got none. With few distinct keys, hashing does not balance anything. Skew like this also appears in production when one key (a very busy tenant, a default value like unknown) dominates, producing a hot partition that one consumer must handle alone. It is the same hot-shard problem described in Databases in System Design.
Replication in one paragraph
Each partition has a replication factor: a leader replica that handles all reads and writes, plus follower replicas on other brokers that copy it. The followers that are caught up form the in-sync replica set (ISR). If the leader's broker dies, one in-sync follower is promoted. A replication factor of 3 is the common production choice because it survives the loss of a broker while leaving two copies; a factor of 1 (fine on a laptop) means a dead disk is lost data. Recent Kafka versions coordinate the cluster with their own KRaft quorum instead of ZooKeeper.
Doing it on a real broker
Kafka is not installed in this course environment, so treat the following as the commands you would run against a local broker listening on port 9092. The scripts ship in the bin folder of the Kafka download; the .sh suffix is part of the name on Linux and macOS, and Windows uses the .bat equivalents under bin\windows.
bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic parcel-events --partitions 3 --replication-factor 1
bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic parcel-eventsOutput
Created topic parcel-events.
Topic: parcel-events PartitionCount: 3 ReplicationFactor: 1
Partition: 0 Leader: 1 Replicas: 1 Isr: 1
Partition: 1 Leader: 1 Replicas: 1 Isr: 1
Partition: 2 Leader: 1 Replicas: 1 Isr: 1The describe output above is abbreviated and illustrative: real output also prints a topic id and a configuration column, and the exact layout varies between versions. What matters is reading it: each partition has a leader broker id, the list of replicas, and the ISR. If Isr is shorter than Replicas, a follower is lagging or down.
Common mistakes
- Treating offsets as global ids. Using the offset alone as a unique key in a database causes silent overwrites, because partition 0 and partition 1 both have an offset 0. Use (topic, partition, offset).
- Expecting global ordering. Developers see ordered output on a one-partition dev topic, then ship to a twelve-partition topic and watch events for the same entity arrive out of order. The symptom is a state machine receiving
ENDEDbeforeSTARTED. Fix it by keying on the entity id. - Sending everything with a null key and then needing per-entity order. Records are spread across partitions with no relation to the entity, so no consumer can rely on order.
- Creating thousands of partitions "for future scale". Each partition costs file handles, replication traffic and recovery time on the brokers, and more partitions do not help if you have only a few consumers.
- Assuming reads delete data. A new engineer "cleans up" a topic by consuming it, then is surprised it is still full. Only retention (or compaction) removes records.
How this shows up in real projects and interviews
In interviews, the opening Kafka question is almost always some form of "how do you guarantee ordering for one user?" and the expected answer is the key-to-partition mapping above, plus the admission that ordering is per partition. In real projects, the first design decision on any new topic is the key. Get it right and consumers can be stateless per record; get it wrong and you end up building reordering logic downstream. Partition count is the second decision, and it is usually set by the maximum parallelism you expect, which the next lesson on consumer groups explains.
Try it yourself
Modify log_sim.py so the topic has 4 partitions and send 40 events using 12 different locker keys (for example locker-1 to locker-12). Print how many records each partition received, then change the key for every event to the same constant string and print the counts again. Write one sentence on what each result would mean for consumer parallelism.
Show a hint
Count with len(topic.parts[p]). With one constant key, every record hashes to the same partition, so only one consumer in a group could ever be busy.
Key takeaways
- A topic is a set of partitions; each partition is an append-only log and the only place order is guaranteed.
- An offset is a record's position inside one partition, so the full address is (topic, partition, offset).
- Reading never deletes records; each reader tracks its own position and retention removes data later.
- The record key chooses the partition, so key choice is really an ordering and load-balancing decision.
- Partitions can be added but not removed, and adding them changes where existing keys map.
Test yourself
A trick from this lesson's ride: An offset is a house number; the partition is the street. Number 7 exists on every street. Four more tricks like it wait along the route, each followed by a question.
Frequently asked questions
What is an offset in Kafka?
An offset is the sequential number Kafka assigns to each record within a partition, starting at 0. It identifies position in that one partition only, so the same offset number exists in every partition of a topic. A consumer stores the next offset it wants to read as its progress marker.
How many partitions should a Kafka topic have?
Choose based on the highest number of consumers in one group you will ever need and on expected throughput, then leave some headroom. More partitions allow more parallel consumers but add broker overhead and cannot be reduced later. There is no universal number; measure with your own message sizes and rates.
Does Kafka guarantee message ordering?
Only within a single partition. Records with the same key go to the same partition and are read in the order they were written, but there is no ordering guarantee between different partitions of a topic. If you need ordering for an entity, use its id as the record key.