Kafka-Style Logs: Topics, Partitions, Offsets
A topic is a set of append-only partition logs; a consumer group splits the partitions and remembers an offset per partition — which gives per-partition ordering, replay from any offset and many independent readers, and makes the partition key and consumer lag the two decisions that determine whether the system behaves.
A queue that deletes on ack can serve one consumer and cannot be re-read. A partitioned log keeps every event for a retention window, lets Inventory, Analytics and a brand-new Fraud service each read the whole stream at their own offset, and spreads a million events per second across partitions while keeping one order’s events in order.
Topics, partitions, offsets
A topic is a name. Its data lives in N partitions, each an append-only file on disk. A record is written to exactly one partition, chosen by hash(key) mod N — or round-robin when there is no key — and receives an offset, its position in that partition. Offsets are per partition: orders-0 has offsets 0, 1, 2, … and so does orders-1; there is no global sequence number across the topic. Brokers replicate each partition to a few others (replication factor 3; a write is acknowledged when the in-sync replicas have it, acks=all).
Nothing is deleted when a consumer reads. Retention deletes by age or size (7 days, 1 TB) regardless of who has consumed. A consumer group is a named set of consumers; each partition is assigned to exactly one member of the group, and the group commits an offset per partition (group inventory: orders-0 → 1842). Two groups reading the same topic are independent — this is how one stream feeds many teams — and a new group starting at earliest gets the whole retained history: replay.
Topic orders (3 partitions, key = orderId) Partition 0 → E1 → E4 → E7 offsets 0,1,2 group inventory @ 3 group analytics @ 1 Partition 1 → E2 → E5 → E8 offsets 0,1,2 group inventory @ 3 group analytics @ 3 Partition 2 → E3 → E6 → E9 offsets 0,1,2 group inventory @ 2 group analytics @ 0 E1 is before E4 is before E7 — guaranteed (same partition) E1 versus E2 — no order at all (different partitions) lag(inventory) = 0 + 0 + 1 = 1 lag(analytics) = 2 + 0 + 3 = 5
The partition key decides what stays ordered
Ordering is guaranteed only within a partition, so the key is the ordering unit. Key by orderId and every event for one order — Created, Paid, Shipped — lands in one partition in write order; consumers see a coherent story per order, and different orders interleave, which nobody cares about. Key by userId and one user’s orders are ordered relative to each other too, but a single very active user (a marketplace reseller, a load test) becomes a hot partition that one consumer must drain alone while the others idle. No key at all spreads records perfectly and maximises throughput, and destroys ordering entirely — OrderUpdated can be read seconds before OrderCreated. That exact change, orderId → random key "to fix a hot partition", is the challenge events-out-of-order.
Changing the partition count changes hash(key) mod N, so events for one key move to a different partition from that moment: old events in partition 2, new ones in partition 5, and ordering across the boundary is lost for in-flight keys. Choose the partition count with headroom up front (partitions are cheap; 12–50 is ordinary) and treat repartitioning as a migration with a drain, not a config toggle.
| Key | Ordering guarantee | Distribution | Typical failure |
|---|---|---|---|
orderId | All events of one order in order | Even; orders are many and small | None common — the usual right answer |
userId | One user’s orders in order across orders | Skewed by heavy users | Hot partition; one consumer lags while others idle |
region / tenantId | Per region / tenant | Very skewed (one big tenant) | One partition holds 60% of traffic; cannot scale it |
| none (round-robin) | None | Perfect | OrderUpdated consumed before OrderCreated |
Consumer lag, rebalancing, compaction
Consumer lag — latest offset minus committed offset, summed over partitions — is the metric that tells you whether the system is keeping up, and it should be on the first dashboard. Lag that grows is a consumer slower than its producer, exactly the situation Backpressure describes; lag on one partition only is a hot key or a stuck consumer. Express it in time too (the timestamp of the oldest unconsumed record): "1.2 M records behind" is meaningless, "38 minutes behind" is an SLO breach.
When a consumer joins or leaves a group — deploy, crash, scale-out — the group rebalances: partitions are reassigned, and during the rebalance the affected partitions are not consumed. A group of 20 consumers redeploying one at a time triggers 20 rebalances; cooperative/incremental assignment and static membership reduce the pause, but rebalances remain a deploy-time source of lag spikes. A group can never use more consumers than there are partitions; the extras sit idle.
Compaction is the alternative to time-based retention: keep only the latest record per key, delete older ones. A compacted customer-profile topic is a changelog whose replay produces the current state of every customer, the same idea as a Hash Map rebuilt from its update history, and the basis for the read models in CQRS. Commit offsets after the effect is durable, and make the effect idempotent: at-least-once with an idempotent consumer is the honest form of exactly-once, here as everywhere (Event-Driven Architecture).
Key points
- A topic is N append-only partitions; an offset is a position in one partition, never a global sequence.
- Ordering holds only within a partition; the partition key is therefore the ordering unit —
orderIdfor orders. - Reads delete nothing: retention deletes by age/size, so many groups can read the same stream and replay from any offset.
- Consumer lag (in records and in minutes) is the health metric; a group cannot exceed one consumer per partition.
- Commit offsets after the effect is durable and keep consumers idempotent; rebalances and redeliveries are normal.
Topics, partitions, offsets, consumer groups
How data moves through it
One request or event, hop by hop.
- 1Order Service → Broker: produce
OrderCreatedwith keyorderId,acks=all; the broker appends to partitionhash(key) mod 3and returns the offset. - 2Broker → Replicas: the partition leader replicates to two followers before acknowledging.
- 3Broker → inventory #2: the consumer assigned
orders-1polls a batch starting at its committed offset 1790. - 4inventory #2 → Inventory DB: apply the batch idempotently (event id in
processed_events), then commit offset 1802. - 5Broker → analytics: the second group reads the same partitions at its own offsets; lag = latest − committed, reported per partition.
- 6Retention job → disk: segments older than 7 days are deleted; a compacted topic keeps the latest record per key instead.
When to use — and when not
- Several teams or services consume the same stream (orders → inventory, analytics, fraud, search indexing).
- You need to replay: rebuild a projection, backfill a new consumer, or reprocess after a bug.
- High throughput (10⁵–10⁶ events/s) with per-key ordering, e.g. per order, per account, per device.
- Change-data-capture from a database (Replication and Read Scaling log → topic) feeding caches and read models.
- Task distribution with per-message acks, priorities and per-message retry delays — a RabbitMQ/SQS-style queue does this natively; Kafka does not.
- Low volume, one consumer, no replay need: running a broker cluster, ZooKeeper/KRaft and lag monitoring is disproportionate.
- Strict total ordering across all events: that is one partition, which caps throughput at one consumer.
- Request/response: a log is not an RPC channel; see Request/Response vs Event-Driven.
Tradeoffs
Near-linear throughput with partitions, durable history and many readers; paid for with a cluster to operate, partition-count decisions that are hard to change, rebalance pauses, and ordering that only exists per key.
How it fails
- Partition key changed from
orderIdto random for throughput:OrderUpdatedconsumed beforeOrderCreated, consumers write nonsense state. - Hot key (
userIdof a reseller doing 40% of orders): one partition lags by hours while the other eleven are idle and autoscaling adds useless consumers. - Offsets committed before processing (auto-commit on a 5 s timer): a crash loses the records between commit and completion.
- Retention shorter than the longest outage: a consumer down for 8 days on a 7-day topic has permanently lost data.
- Partition count increased in production: keys reshuffle and in-flight orders split across two partitions.
How it scales
- Throughput scales with partitions; producers and consumers scale with them up to one consumer per partition per group.
- Add partitions with headroom before you need them; re-keying live traffic is a migration.
- Storage scales with retention × ingest rate; tiered storage or compaction bounds it.
- Hot keys are the ceiling: salt the key (
userId#shard) and accept ordering per sub-key, or route the heavy tenant to its own topic.
How it interacts with databases, queues, caches, APIs and external systems
- Database: consumers commit the effect and the offset (or event id) together; CDC connectors turn the DB write-ahead log into topics.
- Queue: Kafka replaces a queue for streams but is usually paired with a task queue for per-message retry/DLQ semantics.
- Cache: a compacted topic is the canonical way to keep a cache warm and rebuildable; Redis streams offer a lighter version (Redis: Data Structures, Not a Cache).
- APIs: services expose events on topics as their outbound contract, with a schema registry enforcing compatibility.
- External systems: connectors sink topics to warehouses, search indexes and object storage for analytics and replay.