Kafka Fundamentals: Brokers, Topics, Partitions, and Offsets
A staff-engineer guide to Kafka's core architecture — brokers, topics, partitions, offsets, producers, and consumer groups.
Kafka Fundamentals
Apache Kafka is the backbone of event-driven backend systems — it decouples producers from consumers, absorbs traffic spikes, and gives you a durable, replayable log of everything that happened in your system. This guide covers the mental model every backend engineer needs before writing a single line of producer or consumer code: how brokers, topics, partitions, and offsets fit together, and why the partitioning scheme you choose determines both your throughput ceiling and your ordering guarantees.
1. Why Event Streaming, Not Just a Queue
Traditional message queues (RabbitMQ, ActiveMQ, SQS) are built around a consume-and-delete model: a message is removed from the queue once it's acknowledged. Kafka inverts this. It is a distributed, append-only commit log — messages are retained for a configured period (or forever, for compacted topics) regardless of whether they've been read. Multiple independent consumers can replay the same data at different times without interfering with each other.
Message queue vs Kafka
| Aspect | Traditional queue (RabbitMQ/SQS) | Kafka |
|---|---|---|
| Storage model | Message deleted after ack | Append-only log, retained by time/size policy |
| Consumption | Single consumer typically "wins" the message | Every consumer group gets its own full copy |
| Replay | Not supported (once consumed, gone) | Native — rewind offsets, reprocess history |
| Ordering | Per-queue, often best-effort | Strict per-partition ordering |
| Throughput | Good, bounded by broker I/O per queue | Very high — scales horizontally via partitions |
| Use case fit | Task queues, RPC-style work distribution | Event streams, audit logs, CDC, fan-out |
When to reach for Kafka vs a queue: if you need multiple independent teams to consume the same event stream at their own pace, need replay for reprocessing or backfills, or are building an event-sourced/CDC pipeline, Kafka is the right tool. If you just need "do this job once, then discard it," a simpler task queue (SQS, or even a DB-backed job table) is often less operational overhead.
2. Brokers and Clusters
A broker is a single Kafka server process — it stores data on disk, serves produce/fetch requests, and participates in replication. A cluster is a group of brokers coordinating together. Modern Kafka (3.x+ with KRaft) manages cluster metadata via a built-in Raft-based quorum controller, replacing the older ZooKeeper-based coordination.
Each broker owns a subset of partitions as leader and holds follower replicas of others. Clients always produce to and fetch from the current partition leader — never directly from a follower (except with rack-aware "follower fetching" for read locality, a separate optimization).
Discovering the cluster: clients only need a small bootstrap.servers list (2-3 broker addresses) to start. On connect, the client asks any broker for full cluster metadata — partition leaders, replica assignments — and caches it, refreshing on errors like NOT_LEADER_OR_FOLLOWER.
3. Topics and Partitions
A topic is a named, logical stream of events — orders, payment-events, user-signups. Under the hood, a topic is split into one or more partitions, and each partition is an independently ordered, append-only log stored as a set of segment files on disk.
Why partitioning matters
Partitions are the unit of parallelism in Kafka:
- Producer throughput scales because different partitions can be written to different brokers concurrently.
- Consumer throughput scales because each partition can only be read by one consumer within a given consumer group at a time — more partitions means more consumers can work in parallel.
- Ordering is guaranteed only within a partition, never across partitions of the same topic.
# Create a topic with 6 partitions and replication factor 3
kafka-topics.sh --create \
--topic orders \
--bootstrap-server broker1:9092 \
--partitions 6 \
--replication-factor 3 \
--config retention.ms=604800000 \
--config min.insync.replicas=2| Decision | Guidance |
|---|---|
| Partition count | Set to at least your target max consumer parallelism; err slightly high — you can add partitions later, but never remove them |
| Increasing partitions later | Safe for throughput, but breaks key-based ordering for existing keys (their target partition changes) |
| Too many partitions | Increases file handles, memory on brokers, and end-to-end latency for controller operations (leader elections, rebalances) |
| Too few partitions | Caps consumer parallelism — extra consumers in a group just sit idle |
You cannot decrease partition count on an existing topic, and increasing it reshuffles the hash-to-partition mapping for keyed messages. Plan partition count for your peak expected parallelism, not your current traffic — under-provisioning is the far more common production mistake.
4. Offsets and the Log
Every message within a partition gets a monotonically increasing offset — its position in that partition's log. Offsets are partition-local: partition 0's offset 42 and partition 1's offset 42 are unrelated messages.
Anatomy of a log segment
On disk, each partition's log is split into segments — files rolled over by size or time (log.segment.bytes, log.segment.ms). This makes retention cheap: expiring old data means deleting whole segment files, not scanning and rewriting a log.
| Concept | Meaning |
|---|---|
| Log-end offset (LEO) | The offset of the next message to be written |
| High watermark (HW) | The highest offset that has been replicated to all in-sync replicas — the point up to which consumers can read |
| Committed offset | The offset a consumer group has acknowledged as processed, stored in the internal __consumer_offsets topic |
| Retention | retention.ms / retention.bytes — how long/large a segment can grow before eligible for deletion |
| Compaction | Alternative retention: cleanup.policy=compact keeps only the latest value per key, forever — used for changelog/state topics |
# Inspect committed offsets for a consumer group
kafka-consumer-groups.sh --bootstrap-server broker1:9092 \
--describe --group billing-service
# GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# billing-service orders 0 1042 1050 8
# billing-service orders 1 998 998 0LAG is the single most important operational metric in Kafka consumption — it tells you how far behind a consumer group is from the latest produced data.
Offsets are not deleted when a message expires via retention. If a consumer's committed offset falls behind the retained window (e.g., it was down for a week and retention is 3 days), on resume it will hit OffsetOutOfRangeException and, depending on auto.offset.reset, either jump to the earliest available offset or the latest — silently skipping data if not handled carefully.
5. Producers
A producer sends records to a topic. Kafka does not require you to pick a partition explicitly — a partitioner decides which partition a record lands in, based on whether the record has a key.
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, OrderEvent> producerFactory() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "broker1:9092,broker2:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.LINGER_MS_CONFIG, 5);
props.put(ProducerConfig.BATCH_SIZE_CONFIG, 32_768);
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
return new DefaultKafkaProducerFactory<>(props);
}
@Bean
public KafkaTemplate<String, OrderEvent> kafkaTemplate(
ProducerFactory<String, OrderEvent> producerFactory) {
return new KafkaTemplate<>(producerFactory);
}
}How keys determine partitioning
- Keyed records (
key = orderId) always hash to the same partition — this is how you get per-key ordering: every event for a given order lands in the same partition, in send order. - Unkeyed records use Kafka's sticky partitioner (since 2.4+), which batches records for one partition at a time before switching — improving batching efficiency versus strict round-robin, at the cost of no ordering guarantee across records.
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
public OrderEventPublisher(KafkaTemplate<String, OrderEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publish(OrderEvent event) {
// Keying by orderId guarantees all events for this order
// are processed in order by any single consumer.
kafkaTemplate.send("orders", event.orderId(), event)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Failed to publish order event {}", event.orderId(), ex);
} else {
log.debug("Published to partition {} offset {}",
result.getRecordMetadata().partition(),
result.getRecordMetadata().offset());
}
});
}
}Choose partition keys deliberately. Keying by orderId gives per-order ordering and even spread (assuming order IDs are well-distributed). Keying by something low-cardinality like region or tenantTier can create hot partitions — a handful of partitions absorb most of the traffic while others sit idle, capping your effective throughput regardless of partition count.
Batching and linger
Producers don't send one record per network call — they buffer records per partition into batches, controlled by batch.size (max bytes per batch) and linger.ms (max time to wait for a batch to fill before sending anyway). This trade-off is the classic latency-vs-throughput knob.
| Setting | Low value | High value |
|---|---|---|
linger.ms | Lower latency, smaller batches, more network overhead | Higher latency, bigger batches, better throughput/compression |
batch.size | More frequent, smaller sends | Fewer, larger sends — better compression ratio |
compression.type | none — lowest CPU, largest network payload | lz4/zstd — smaller payload, some CPU cost |
6. Consumers and Consumer Groups
A consumer group is a set of consumer instances that cooperatively consume a topic, splitting the partitions among themselves. Each partition is assigned to exactly one consumer within a group at any moment — this is what makes horizontal scaling of consumption safe without duplicate processing within the group.
The golden rule of consumer scaling
Consumers in a group ≤ partitions in the topic. Extra consumers beyond the partition count sit completely idle — they never get assigned a partition.
| Consumers vs partitions | Effect |
|---|---|
| Consumers < partitions | Some consumers own multiple partitions — fine, just less parallel |
| Consumers = partitions | Perfectly balanced — 1:1 partition-to-consumer |
| Consumers > partitions | Excess consumers are idle — wasted resources, no benefit |
Sizing a consumer group larger than the topic's partition count is one of the most common Kafka misconfigurations. If you need more consumption parallelism than your current partition count allows, you must increase partitions — but remember that breaks the key-to-partition mapping for existing keyed data.
Independent consumer groups get independent copies
Each consumer group tracks its own committed offsets. Two different groups reading the same topic don't affect each other at all — this is how Kafka supports fan-out to multiple independent applications from one topic.
@Component
public class OrderEventListener {
@KafkaListener(
topics = "orders",
groupId = "billing-service",
concurrency = "3" // spins up 3 consumer threads within this app instance
)
public void onOrderEvent(
@Payload OrderEvent event,
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
@Header(KafkaHeaders.OFFSET) long offset) {
log.info("Processing order {} from partition {} offset {}",
event.orderId(), partition, offset);
billingService.charge(event);
}
}concurrency in Spring Kafka creates multiple consumer threads inside a single application instance, each behaving as an independent consumer within the group. It's subject to the same rule: threads beyond the partition count (accounting for other instances/pods also in the group) sit idle.
7. Ordering Guarantees
Kafka's ordering guarantee is precise and easy to misstate: records are ordered within a partition, in the order the producer sent them (assuming no retries reorder them — see idempotent producers in the next guide), and consumed in that same order by whichever consumer owns the partition. There is no ordering guarantee across partitions, even within the same topic.
This is why key selection is an ordering decision, not just a load-balancing one. If your business logic requires "all events for order X must be processed in the order they occurred," you must key by orderId (or whatever entity needs ordering) — never rely on global ordering across a multi-partition topic, because it does not exist.
A common production bug: switching a topic's producer from unkeyed to keyed (or changing the key field) silently changes which partition future messages for the same entity land in, while old messages for that entity remain in the old partition. Consumers processing both old and new partitions for the same entity can see out-of-order events during the transition. Plan key changes as a careful migration, not a config tweak.
8. Production Observations
- Partition count is a one-way door for scaling up. Choose it based on your projected peak consumer parallelism, not today's traffic — you can't shrink it later without recreating the topic.
- Lag is your primary health signal. Alert on consumer group lag (in messages and in time-behind), not just on broker uptime — a healthy cluster with a stuck consumer looks fine from the broker's perspective.
- Hot partitions kill throughput silently. A poorly chosen key (low cardinality, skewed distribution) caps your effective parallelism even with plenty of partitions provisioned.
bootstrap.serversneeds only a few broker addresses — full cluster topology is discovered dynamically, so you don't need every broker listed, just enough for initial connectivity redundancy.- Retention and consumer downtime interact. If a consumer group can be down longer than
retention.ms, it will lose data on resume (viaauto.offset.reset) unless you explicitly monitor and alert on approaching retention boundaries.
Key takeaways
- Kafka is a durable, replayable, partitioned log — not a delete-on-read queue; multiple consumer groups can independently replay the same topic.
- Partitions are the unit of both producer throughput and consumer parallelism; consumer count beyond partition count is wasted capacity.
- Ordering is guaranteed only within a partition; keyed records deterministically hash to the same partition, giving per-key ordering.
- Offsets are partition-local and tracked per consumer group in
__consumer_offsets; lag (LOG-END-OFFSET minus CURRENT-OFFSET) is the key operational metric. - Producer batching (
linger.ms,batch.size,compression.type) trades latency for throughput and compression efficiency. - Increasing partitions on an existing topic is safe for scaling but silently changes the key-to-partition mapping — plan around it.
- Retention windows and consumer downtime must be reasoned about together, or you risk silent data loss via
auto.offset.reset.
Interview Questions
- What is the difference between a Kafka topic and a partition? Why do partitions exist?
- How does Kafka guarantee ordering, and why is that guarantee scoped to a single partition?
- What determines which partition a producer record is sent to? How does keying affect this?
- What happens if you add more consumer instances to a group than there are partitions?
- Explain the difference between a topic's log-end offset and a consumer group's committed offset.
- What is consumer lag, and how would you monitor and alert on it in production?
- Why can you not decrease the partition count of an existing topic? What happens when you increase it?
- What is a hot partition, and how would you diagnose and fix one?
- How does Kafka differ fundamentally from a traditional message queue like RabbitMQ or SQS?
- What role does
linger.msandbatch.sizeplay in producer performance? What trade-off do they represent? - What happens to a consumer group's offsets if it's down longer than the topic's retention period?
- Explain how the sticky partitioner behaves differently from strict round-robin for unkeyed records.
- What is log compaction, and when would you choose
cleanup.policy=compactover time/size-based retention? - Why does KRaft (Kafka's built-in Raft consensus) replace ZooKeeper in modern Kafka clusters, at a high level?