Unit 2.3 · Kafka and Messaging
Replication, ISR, Retention, Compaction, and Running Kafka
How Kafka survives broker failure (replication factor, ISR, min.insync.replicas, unclean election), retention vs compaction, sizing partitions, schemas, and operating Kafka with Strimzi or MSK.
Start here
The mental model
Every partition is copied to several brokers (the REPLICATION FACTOR, usually 3). One copy is the leader; the followers keep up by fetching from it. The followers that are fully caught up form the IN-SYNC REPLICA set (ISR). A write with acks=all is only confirmed once every ISR member has it, so if the leader dies, any ISR member can take over with nothing lost.
The safety catch is min.insync.replicas: 'refuse writes unless at least this many copies are in sync'. With RF=3 and min ISR=2, you can lose one broker and keep writing safely; lose two and Kafka stops accepting acks=all writes rather than pretend they're safe.
Go deeper
How it works inside
01ISR, high watermark, and leader election
A follower drops out of the ISR if it hasn't caught up within replica.lag.time.max.ms (30 s). Consumers only see records up to the HIGH WATERMARK, the offset replicated to all ISR members, so they never read data that could disappear in a failover.
If the leader dies, the controller elects a new leader from the ISR. If NO ISR member is alive, there are two bad choices: wait (the partition is unavailable) or allow UNCLEAN leader election, promoting an out-of-sync replica and losing the records it missed. unclean.leader.election.enable=false (the default) chooses availability loss over data loss, which is right for business data.
02Retention vs compaction
DELETE retention (default) removes whole segments older than retention.ms or beyond retention.bytes per partition. Good for event streams: 'keep 7 days of orders'.
COMPACTION (cleanup.policy=compact) keeps at least the LATEST record for every key and removes older ones in the background, forever. A compacted topic is a changelog/table: 'the current price of every product', 'the latest profile of every user'. Deleting a key needs a TOMBSTONE (a record with that key and a null value). Kafka's own __consumer_offsets is compacted. The two policies can be combined (compact,delete).
03Sizing partitions and brokers
Partitions = max(target throughput ÷ per-partition producer throughput, target throughput ÷ per-consumer throughput, the consumer parallelism you'll need), with headroom. You can add partitions later but that remaps keys (breaking per-key order during the change), and you can't reduce them. Too many partitions per broker means slower failover and more open files and memory; with KRaft, clusters handle far more than ZooKeeper-era limits, but thousands per broker still need care.
Spread replicas across availability zones with RACK AWARENESS (broker.rack) so losing a zone never loses all copies of a partition, and consumers can fetch from the closest replica (client.rack) to cut cross-AZ data transfer costs, which on AWS can exceed the cost of the brokers themselves.
04Schemas, Connect, and the ecosystem
Producers and consumers evolve independently, so the record format is a contract. SCHEMA REGISTRY (Confluent, Apicurio, AWS Glue) stores Avro/Protobuf/JSON schemas and enforces COMPATIBILITY rules (e.g. BACKWARD: new consumers can read old data, so only add optional fields). KAFKA CONNECT moves data in and out without custom code: Debezium streams database changes (CDC) from Postgres's WAL into Kafka, and sink connectors write to S3, OpenSearch, or warehouses. Stream processors (Kafka Streams, Flink) join, aggregate, and transform streams.
05Operating Kafka
MSK (and MSK Serverless) or Confluent Cloud remove broker operations. On Kubernetes, STRIMZI is the standard operator: Kafka, KafkaTopic, and KafkaUser CRDs, so topics and ACLs live in Git (GitOps course). The metrics that matter: under-replicated partitions (should be 0), under-min-ISR partitions (0), offline partitions (0), active controller count (exactly 1), request latency p99, disk usage per broker, consumer lag per group, and network throughput. Rolling restarts go one broker at a time, waiting for under-replicated partitions to return to 0 in between.
Do it
Hands-on lab
- 1
A three-broker cluster with Strimzi
On a kind cluster, install the Strimzi operator, then declare the cluster. Node pools define broker and controller roles.
kafka-cluster.yamlwhole fileyaml apiVersion: kafka.strimzi.io/v1beta2 kind: KafkaNodePool metadata: name: dual labels: { strimzi.io/cluster: shop } spec: replicas: 3 roles: [controller, broker] storage: { type: persistent-claim, size: 5Gi, deleteClaim: false } --- apiVersion: kafka.strimzi.io/v1beta2 kind: Kafka metadata: name: shop spec: kafka: listeners: - { name: plain, port: 9092, type: internal, tls: false } config: default.replication.factor: 3 min.insync.replicas: 2 unclean.leader.election.enable: false auto.create.topics.enable: false entityOperator: { topicOperator: {}, userOperator: {} } --- apiVersion: kafka.strimzi.io/v1beta2 kind: KafkaTopic metadata: name: orders labels: { strimzi.io/cluster: shop } spec: partitions: 6 replicas: 3 config: { retention.ms: 604800000 }terminal$ kubectl create ns kafka && kubectl -n kafka apply -f https://strimzi.io/install/latest?namespace=kafkakubectl -n kafka apply -f kafka-cluster.yaml && kubectl -n kafka wait kafka/shop --for=condition=Ready --timeout=600skubectl -n kafka exec shop-dual-0 -- bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders | head -3── expected output ──kafka.kafka.strimzi.io/shop condition metTopic: orders PartitionCount: 6 ReplicationFactor: 3 Configs: min.insync.replicas=2,retention.ms=604800000Topic: orders Partition: 0 Leader: 0 Replicas: 0,1,2 Isr: 0,1,2Topic: orders Partition: 1 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0 - 2
Kill a broker, keep writing
Delete one broker pod while producing. Leadership moves, the ISR shrinks to two, and
acks=allwrites keep succeeding. When the pod returns it catches up and rejoins the ISR.terminal$ kubectl -n kafka delete pod shop-dual-1kubectl -n kafka exec shop-dual-0 -- bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic orders --under-replicated-partitions── expected output ──Topic: orders Partition: 1 Leader: 2 Replicas: 1,2,0 Isr: 2,0Topic: orders Partition: 4 Leader: 0 Replicas: 1,0,2 Isr: 0,2 - 3
A compacted topic as a table
Product prices keyed by SKU. After compaction runs, only the last price per SKU remains. Low segment settings make compaction happen quickly in the lab.
terminal$ kubectl -n kafka exec -i shop-dual-0 -- bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic prices \--config cleanup.policy=compact --config segment.ms=10000 --config min.cleanable.dirty.ratio=0.01printf 'sku-1:499\nsku-2:120\nsku-1:449\nsku-1:399\n' | kubectl -n kafka exec -i shop-dual-0 -- bin/kafka-console-producer.sh \--bootstrap-server localhost:9092 --topic prices --property parse.key=true --property key.separator=:# ~1 minute later, after a new segment rolls:kubectl -n kafka exec shop-dual-0 -- bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic prices --from-beginning --property print.key=true --timeout-ms 5000── expected output ──sku-2 120sku-1 399Processed a total of 2 messages
Operate it
Knobs that matter
| Setting | Default | What it does | When to change it |
|---|---|---|---|
| default.replication.factor | 1 | Copies of each partition for new topics. | 3 in production, with brokers in 3 AZs. |
| min.insync.replicas | 1 | Minimum in-sync copies for acks=all writes. | 2 with RF=3: tolerate one broker loss without risking data. |
| unclean.leader.election.enable | false | Allow an out-of-sync replica to become leader. | Keep false for business data; true only where availability beats completeness (some metrics). |
| cleanup.policy | delete | Time/size retention or per-key compaction. | compact for changelog/state topics; compact,delete to also age out keys. |
| broker.rack / client.rack | unset | AZ awareness for replica placement and nearest-replica fetching. | Set to the AZ ID on every broker and consumer on AWS to survive zone loss and cut cross-AZ cost. |
| log.retention.bytes | -1 | Per-partition size cap. | Set as a disk-full safety net alongside time retention. |
3am practice
Failure drills
Each drill is a real failure mode. Read the scenario and the output, decide what's wrong, then reveal the diagnosis.
Drill #1
Producers fail with NotEnoughReplicas
Two of three brokers are unhealthy after a bad node upgrade. Checkout's event publishing starts failing.
Drill #2
Broker disk 100% full
A team switched a high-volume topic's retention to 30 days 'just in case'. A week later one broker crashes and won't start.
Drill #3
Consumers break after a producer deploy
The orders team renames a field in their JSON event. Three downstream services start failing to process orders.
The bigger picture
Connects to
System Design · Outbox Pattern
Guarantee the DB write and the event publish happen together — via a table that IS the queue.
System Design · Event Sourcing
Store the history of changes as an append-only event log — the state is derived, never stored as a mutable row.
GitOps · Every deployment strategy
Strimzi CRDs let topics and users be deployed through GitOps too.
Unit 1.3 · Expand/contract migrations
The same compatibility discipline applies to event schemas.
Unit 0.1 · Quorums
RF=3 with min ISR 2 is a quorum write.
Observability · Long-term storage
Retention and cost trade-offs look the same for metrics and logs.
Prove it
Interview questions
Explain ISR and min.insync.replicas.
When would you use a compacted topic?
What Kafka metrics do you alert on?