Kafka SDE-2 Handbook
Apache Kafka from first principles to SDE-2 depth in 21 modules, 10 case studies, and 19 hands-on labs. Every lesson opens with a question to try, flow diagrams step through one arrow at a time, labs run on a real cluster on your laptop, and a tracker brings designs back for spaced review.
Loading your progress.
How this handbook teaches
The goal is not to memorize configuration names. It is to understand what Kafka does at each step well enough to predict its behavior, choose settings on purpose, design with it, and debug it at 2 AM. So the handbook asks you to think before it explains, and to run things before you trust them.
- Try before you read. Every lesson opens with a short question. Answer it on paper first; getting it wrong is what makes the explanation stick.
- Analogy, then mechanism, then trade-off. Each idea starts with the problem it solves and a plain analogy, then how it works, then what it costs. Rarely needed details sit in folded "Go deeper" sections.
- Step through every flow. Diagrams with a step bar reveal one arrow at a time. Predict the next arrow before pressing Next.
- Run it. Labs use the official Apache Kafka Docker image and the command-line tools, so you see offsets, lag, rebalances, and failovers happen. Each lab ends with "Check what you saw".
- Live tools. A key-to-partition explorer that uses Kafka's real murmur2 hash, a partition count calculator, a durability explorer for
acksand min ISR, a consumer group playground, a cluster sizing calculator, and a lag drain calculator. - Notes for work and interviews. Green "In production" and "At work" boxes show how ideas appear in real systems, including payments. Purple "Interview signal" boxes say what interviewers listen for at SDE-2 and what a senior answer adds.
How to study one module
- Clear the revision queue above at the start of each session: redesign those systems from a blank page.
- Do the warm-up quiz without reading anything. Note what you missed.
- For each lesson: answer "Try it first", read, step through the figure, then say out loud what problem the idea solves and when it fails.
- Do the lab. Predict the output of each command before running it.
- Do the self-check quiz, then tick lessons as studied.
- Tick the mastery checklist only when each item is true with your notes closed.
How to do the labs
- Set up once on the lab setup page: a single broker for most labs, a three-broker cluster for replication and failure labs.
- Labs take 15 to 30 minutes. They are written for Kafka 4.3 in Docker, with commands you can paste.
- Tick Done on a lab when you have run it and can explain the "Check what you saw" answers. The dashboard counts finished labs.
- Break things on purpose: stop brokers, kill consumers, reset offsets. That is what the local cluster is for.
How to practice a case study
- Set a timer for the time on the case study and design it on paper first: requirements, estimates, topics and keys, the diagram, then the hard parts.
- Open the case study and compare section by section. Answer each guided deep dive before revealing it.
- Set an honest status at the top of the page. The handbook schedules a review.
- When it comes due, redesign it from memory, faster each time. Then try related practice prompts in the tracker.
Design statuses and the revision queue
| Status | Meaning | Next review |
|---|---|---|
| Not started | Not attempted | Not scheduled |
| Could not design it | You needed the solution for the core | 2 days |
| Designed with hints | You got there with a few peeks | 4 days |
| Designed independently | Full design in time, trade-offs explained | 10 days |
| Needs revision | Did it before, but a redo went badly | 1 day |
| Can teach it | Designed it twice, weeks apart, and can answer every follow-up | 30 days |
When a design comes up for review, redesign it without notes, then set the new status or press "Reviewed, same status" to restart its timer.
Where your progress is saved
Lesson ticks, lab ticks, mastery checks, readiness checks, and design statuses save automatically to your account, privately: nobody else who opens this page sees them, and they follow you to other devices. The indicator in the top bar shows where the latest change went. If the page cannot reach your account (for example in a private window), it saves in this browser instead, and the tracker has a backup and restore box.
Version and scope
Written for Apache Kafka 4.3 (October 2026) and checked against its source and documentation. Kafka 4.x runs only in KRaft mode: ZooKeeper was removed in 4.0. Where behavior changed in a recent release (the new consumer group protocol, share groups, linger.ms defaults, eligible leader replicas), a version note says so, so you can recognize older setups at work.
Covered: everything a backend engineer needs to build, run, and debug Kafka-based systems, and what SDE-2 interviews test about Kafka and event-driven design: the data model, producers, consumers, delivery semantics, internals that explain behavior, transactions, schemas, Connect, Streams and Flink at design level, Spring for Apache Kafka, tuning, operations, and troubleshooting.
Left out on purpose: ZooKeeper-mode administration, vendor-specific products in depth, Kafka's source code internals beyond what explains behavior, and non-Java client details (except where they behave differently, such as librdkafka's default partitioner).
Syllabus and prerequisite map
Twenty-one modules in six phases, then ten case studies. Each phase uses ideas from the one before, so the default route is simply top to bottom. Short paths for specific goals are at the end.
The map
Priority overview
| Module | Learn first | Why it matters | Interview weight | Time |
|---|---|---|---|---|
| 1 Why Kafka exists | none | The problem Kafka solves; queue vs log | Every Kafka discussion | 1 h |
| 2 Topics, partitions, offsets | 1 | The core data model | Very high | 1.5 h |
| 3 Brokers, clusters, KRaft | 2 | How the cluster is organized; listeners | High | 1.5 h |
| 4 The producer | 2, 3 | Batching, compression, errors | High | 2 h |
| 5 Durability and ordering | 4 | acks, min ISR, idempotence |
Very high | 2 h |
| 6 Keys and partitioning | 4 | Ordering, hot keys, partition counts | Very high | 2 h |
| 7 Consumers and groups | 2 | Poll loop, groups, scaling | Very high | 2 h |
| 8 Offsets and delivery | 7 | Commits, delivery semantics, replay | Very high | 2 h |
| 9 Rebalancing and share groups | 7, 8 | Rebalance protocols, queues on Kafka | High | 2.5 h |
| 10 Consumer failure handling | 8 | Retries, DLTs, idempotent consumers | High | 2 h |
| 11 Storage on disk | 2 | Segments, retention, why Kafka is fast | Medium | 1.5 h |
| 12 Replication and failover | 3, 5 | ISR, high watermark, elections | High | 2.5 h |
| 13 Log compaction | 11 | Latest value per key; state topics | Medium | 1.5 h |
| 14 Exactly-once and transactions | 5, 8, 12 | Transactions, outbox | Very high in fintech | 2.5 h |
| 15 Schemas and event design | 4, 7 | Contracts, compatibility | Medium | 1.5 h |
| 16 Kafka Connect and CDC | 13, 15 | Integration without custom code | Medium | 2 h |
| 17 Kafka Streams and Flink | 9, 13, 14 | Stateful processing, windows | Medium | 3 h |
| 18 Spring for Apache Kafka | 4 to 10, 14 | How most Java services use Kafka | High at work | 2.5 h |
| 19 Performance and tuning | 4, 7, 12 | Trade-offs, sizing, benchmarks | Medium | 2 h |
| 20 Operations and security | 12, 19 | Monitoring, changes, security, DR | Medium | 2.5 h |
| 21 Troubleshooting playbook | 4 to 12, 20 | Fixing real incidents | High | 2 h |
Total: about 42 hours including labs, plus about 12 hours for the ten case studies (40 to 50 minutes to design each, then about 30 minutes to compare and note what you missed).
Short paths for specific goals
| Goal | Path |
|---|---|
| Interview in two weeks | Modules 1, 2, 4 to 8, 10, 12, 14, then case studies 1, 2, 3, 7, and the interview page |
| Joining an on-call rotation | Modules 7 to 10, 12, 20, 21, and the cheat sheets |
| Building a Spring Boot service | Modules 4 to 8, 10, 15, 18 |
| Designing a data pipeline | Modules 6, 13, 15 to 17, case studies 4, 5, 6 |
| Building payment or order flows | Modules 5, 8, 10, 14, case studies 1, 2, 10 |
| Planning disaster recovery | Modules 12, 20, case study 9 |
How each module is organized
- Warm-up quiz to expose what you do not know yet.
- Lessons, each opening with a "Try it first" question, then the idea, an analogy where it helps, how it works, and the trade-offs.
- Figures that step through flows one arrow at a time.
- Labs you run on a local cluster, each ending with "Check what you saw".
- Go deeper folds for details you need only sometimes; In production, At work, and Interview signal notes; Version notes where behavior changed in recent releases.
- Common mistakes, a self-check quiz, and a mastery checklist.
Case studies follow one format: try it first, requirements, estimates, topics and keys, architecture figure, guided deep dives, failures, what a strong answer shows, and follow-up questions.
Lab setup
Every module has hands-on labs. Set up once here: a single broker for most labs, a three-broker cluster for replication and failure labs, and a Java project for the client code. Everything runs locally in Docker with the official Apache Kafka image.
What you need
- Docker 20.10.4 or newer (Docker Desktop on macOS or Windows, Docker Engine on Linux), with about 4 GB of memory for the three-broker cluster.
- For the Java examples: JDK 17 or newer and Maven or Gradle. Kafka 4.x clients need Java 11 or newer; brokers and tools need Java 17 or newer.
- About 2 GB of free disk.
Labs use Apache Kafka 4.3 (the current release in October 2026) and its option names. Kafka 4.2 renamed some console tool options; on 4.1 or older, use --property instead of --reader-property and --formatter-property, and --producer-property or --consumer-property instead of --command-property.
A single broker
One container runs a combined broker and controller with default settings, listening on localhost:9092.
docker run -d --name broker -p 9092:9092 apache/kafka:4.3.0Kafka's command-line tools live in /opt/kafka/bin inside the container. Open a shell there and run tools with ./:
docker exec -it --workdir /opt/kafka/bin broker sh
./kafka-topics.sh --bootstrap-server localhost:9092 --listStop and delete it when you are done (this also deletes its data):
docker rm -f brokerA three-broker cluster
Used in Modules 3, 5, 12, and 20. Save this as docker-compose.yml. It is the official Apache Kafka combined-mode example with three nodes; each node is both a broker and a controller. Inside the Docker network, nodes talk on port 19092; from your laptop, use localhost:29092, localhost:39092, and localhost:49092.
x-kafka-env: &kafka-env
KAFKA_PROCESS_ROLES: 'broker,controller'
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: 'CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT'
KAFKA_CONTROLLER_QUORUM_VOTERS: '1@kafka-1:9093,2@kafka-2:9093,3@kafka-3:9093'
KAFKA_LISTENERS: 'PLAINTEXT://:19092,CONTROLLER://:9093,PLAINTEXT_HOST://:9092'
KAFKA_INTER_BROKER_LISTENER_NAME: 'PLAINTEXT'
KAFKA_CONTROLLER_LISTENER_NAMES: 'CONTROLLER'
CLUSTER_ID: '4L6g3nShT-eMCtK--X86sw'
KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
KAFKA_LOG_DIRS: '/tmp/kraft-combined-logs'
services:
kafka-1:
image: apache/kafka:4.3.0
hostname: kafka-1
container_name: kafka-1
ports: ["29092:9092"]
environment:
<<: *kafka-env
KAFKA_NODE_ID: 1
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-1:19092,PLAINTEXT_HOST://localhost:29092'
kafka-2:
image: apache/kafka:4.3.0
hostname: kafka-2
container_name: kafka-2
ports: ["39092:9092"]
environment:
<<: *kafka-env
KAFKA_NODE_ID: 2
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-2:19092,PLAINTEXT_HOST://localhost:39092'
kafka-3:
image: apache/kafka:4.3.0
hostname: kafka-3
container_name: kafka-3
ports: ["49092:9092"]
environment:
<<: *kafka-env
KAFKA_NODE_ID: 3
KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://kafka-3:19092,PLAINTEXT_HOST://localhost:49092'Start it, then define a helper that runs any tool on kafka-1 against the cluster:
docker compose up -d
k() { tool=$1; shift; docker exec -it kafka-1 /opt/kafka/bin/$tool --bootstrap-server kafka-1:19092 "$@"; }
k kafka-topics.sh --listThe helper puts --bootstrap-server right after the tool name, which every Kafka tool accepts. Internal topics such as __consumer_offsets use replication factor 3 by default, which this cluster satisfies. Stop and remove everything with docker compose down.
A Java project
Add the Kafka client library. The client version can be newer than your brokers; Kafka 4.x clients work with brokers 2.1 and newer.
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>4.3.0</version>
</dependency>implementation 'org.apache.kafka:kafka-clients:4.3.0'For Spring Boot (Module 18), add org.springframework.kafka:spring-kafka and let Spring Boot manage its version.
Optional tools
- A web UI helps you browse topics and consumer groups. Open-source options include Kafbat UI, AKHQ, and Redpanda Console. None is required; every lab uses the bundled command-line tools.
- kcat (formerly kafkacat) is a fast command-line producer and consumer, handy for scripts.
Command-line tools you will use
| Tool | Used for | First appears |
|---|---|---|
kafka-topics.sh |
Create, describe, alter, delete topics | Module 2 |
kafka-console-producer.sh, kafka-console-consumer.sh |
Write and read records by hand | Module 2 |
kafka-consumer-groups.sh |
Group members, offsets, lag, offset resets | Module 7 |
kafka-configs.sh |
Read and change topic, broker, and client configs | Module 5 |
kafka-producer-perf-test.sh, kafka-consumer-perf-test.sh |
Load tests | Module 4 |
kafka-dump-log.sh |
Inspect segment files on disk | Module 11 |
kafka-leader-election.sh, kafka-reassign-partitions.sh |
Move leadership and replicas | Modules 3, 20 |
kafka-metadata-quorum.sh |
Controller quorum status | Module 3 |
kafka-transactions.sh |
List and abort transactions | Module 14 |
Module 1: Why Kafka exists
Before configs and APIs, learn the problem Kafka solves. This module shows what breaks when services call each other directly, what a log is, how Kafka gives you both a queue and pub/sub, where it fits in real systems, and when it is the wrong tool.
- Checkout calls email, fraud, and analytics over HTTP, one after another. Analytics gets slow. What happens to checkout, and what is the event-driven fix?
Reveal
Checkout becomes as slow as its slowest dependency and fails when any of them fails. Publish one
PaymentCompletedevent to Kafka instead; email, fraud, and analytics each consume it at their own pace, and checkout returns as soon as the event is written. - After a consumer reads a message from Kafka, is the message deleted?
Reveal
No. It stays until the topic's retention removes it (7 days by default). Other consumers, and the same consumer later, can read it again.
- Ten independent services must each receive every order event. Do you need ten copies of the topic?
Reveal
No. Each service uses its own consumer group, and every group reads the whole topic independently.
- Name one job Kafka is a poor fit for.
Reveal
Synchronous request-reply where the caller needs the answer now; per-message delays or priorities; storing large files; or a small job queue where a managed queue is simpler to run.
- What makes Kafka fast enough for millions of messages per second?
Reveal
Appending to sequential files, batching many records per request, compression, serving reads from the operating system's page cache, and spreading partitions across many brokers.
1.1The problem: services calling each other
The payments service must notify email, fraud scoring, the ledger, and analytics. Today it calls each one over HTTP. List three things that go wrong as traffic and the number of consumers grow.
Reveal the answer
- Latency adds up: the payment waits for four calls in a row.
- Availability multiplies: four services at 99.5% each give at most 0.995^4, about 98.0%, which is roughly a week of failed payments a year.
- Failures spread: if the ledger is down, the payment either fails or needs custom retry code.
- Bursts overload the slowest service: a sale doubles traffic and analytics falls over.
- Every new consumer means changing and redeploying payments.
- No history: if fraud scoring had a bug yesterday, there is nothing to replay.
Put a durable log between the services. Producers write each event once; consumers read it whenever they are ready. Neither side needs to know the other exists or be up at the same time.
Instead of phoning every department about a new hire, HR pins one notice to a board that keeps every notice in order. Each department reads it when it can, and a department that opens next year can still read the old notices.
Decoupling, burst absorption, independent scaling, many consumers for one event, and a replayable history.
Results arrive later (eventual consistency), consumers must handle duplicates, and you run one more distributed system.
Commands vs events. A command asks for something to happen ("SendEmail") and has one intended handler. An event states a fact ("PaymentCompleted") that any number of services can react to. Kafka works best with events, because the producer does not need to know who listens, so adding a consumer never touches the producer.
1.2The log: Kafka's core idea
A bank passbook only gets new lines at the bottom, every line has a number, and you can re-read from any line. Which properties let you and the bank reconcile your balance independently?
Reveal the answer
It is append-only (old lines never change), ordered with positions (line numbers), and readable from any position without affecting anyone else. Two readers can each remember "I have checked up to line 40" and compare later.
A log is an append-only, ordered sequence of records. Each record gets a position called an offset. Writers add to the end; each reader remembers its own offset.
The passbook, or an accounting ledger. Entries are never edited; a correction is a new entry.
Readers do not interfere with each other, any reader can replay history, and the current state of something (a balance) can be rebuilt by reading its events from the start.
Kafka is a distributed commit log. It takes this simple structure and splits each log into partitions spread across many servers, with copies on several servers so a crash loses nothing. Databases already use a log internally (the write-ahead log); Kafka makes the log itself the product that other systems read.
- Events are facts:
PaymentAuthorized,CardBlocked. They are immutable. - State is derived: a balance is the sum of its events. Module 13 shows topics that keep only the latest value per key, which turns a log into a table.
1.3Queue, pub/sub, and log
You want 20 workers to share image-resize jobs. Separately, email, search, and audit must each receive every new-user event. Which messaging shape fits each need?
Reveal the answer
Resize jobs need a queue: each job goes to exactly one worker. New-user events need pub/sub: every subscriber gets its own copy. Kafka covers both: put the 20 workers in one consumer group (they split the work) and give email, search, and audit one group each (each group gets everything).
| Queue | Pub/sub | Log (Kafka) | |
|---|---|---|---|
| Who gets a message | One consumer | Every subscriber | Every consumer group; one member per partition inside a group |
| After it is read | Deleted | Deleted per subscriber | Kept until retention expires |
| Replay | No | Usually no | Yes: move the offset back |
| Ordering | Weak once workers run in parallel | Per subscriber, often none | Strict within a partition |
| Examples | RabbitMQ queues, Amazon SQS | Amazon SNS, RabbitMQ fanout | Kafka, Amazon Kinesis, Pulsar |
One mechanism gives both behaviors: inside a group, partitions are divided (queue); across groups, everything is shared (pub/sub). Module 7 covers groups in depth.
1.4What Kafka is, on one page
| Term | One-line meaning | Module |
|---|---|---|
| Record (event, message) | Key, value, headers, timestamp | 2 |
| Topic | A named stream of records | 2 |
| Partition | One ordered, append-only log; a topic has many | 2 |
| Offset | A record's position in its partition | 2, 8 |
| Broker | A Kafka server that stores partitions and serves clients | 3 |
| Controller (KRaft) | The nodes that manage cluster metadata and leader elections | 3 |
| Replica, leader, follower | Copies of a partition; the leader takes writes, followers copy it | 3, 12 |
| Producer | Client that writes records | 4 to 6 |
| Consumer, consumer group | Client that reads; a group splits partitions among its members | 7 to 10 |
| Kafka Connect | Framework that moves data between Kafka and other systems | 16 |
| Kafka Streams | Java library for processing streams with state | 17 |
Four properties come out of this design:
- Durable: records are written to disk on several brokers before they count as committed.
- Scalable: more partitions spread load across more brokers and more consumers.
- Ordered where it matters: strict order within a partition, chosen by the record key.
- Replayable: records stay for the retention period, so any consumer can re-read history.
1.5Where Kafka fits
- Event-driven microservices: services publish facts; others react. New features subscribe without changing the producer.
- Activity tracking and analytics: clicks, page views, and app events at very high volume.
- Log and metrics pipelines: agents ship logs to Kafka, which buffers them for search and storage systems.
- Data integration and change data capture (CDC): stream every database change to caches, search indexes, and the data warehouse.
- Stream processing: compute running totals, detect fraud patterns in time windows, and join streams as events arrive.
- Audit trail and event sourcing: an ordered, immutable history of what happened.
- Buffering: absorb bursts between fast producers and slow sinks.
In a card platform, the authorization service publishes every authorization to one topic keyed by card ID. Fraud scoring, the ledger, customer notifications, rewards, and the data warehouse each consume it with their own consumer group. A new team (say, spend analytics) subscribes on day one and replays last week's events to backfill.
1.6When not to use Kafka
For each, Kafka or something else? (a) A mobile app needs a balance check that returns in 50 ms. (b) Send a reminder exactly 24 hours after signup. (c) Store 200 MB scanned KYC documents. (d) Five background jobs a minute for a small team on AWS.
Reveal the answer
(a) Something else: synchronous request-reply belongs on HTTP or gRPC. (b) Kafka has no per-message delay; use a scheduler, or a delay pattern (Module 10, Case study 8). (c) Store files in object storage and send a reference in the event (claim check, Module 15). (d) A managed queue such as SQS is simpler; Kafka is overkill unless it is already your platform.
- Request-reply that needs an answer now. You can build it on Kafka, but it adds latency and complexity for no gain.
- Per-message delays, priorities, or complex routing rules. RabbitMQ and SQS have these built in; on Kafka you build them from topics (Module 10 and Case study 8).
- Large payloads. The default limit is about 1 MB per record batch. Put big objects in object storage.
- Ad hoc queries. Kafka has no index by key or field. You cannot ask "all events for account 42" efficiently; sink the data into a database for that.
- Global ordering at high throughput. Total order means one partition, which caps throughput at what one partition can handle.
- Tiny workloads or no team to run it. Use a managed service (Confluent Cloud, Amazon MSK, Aiven) or a simpler queue.
Kafka 4.2 made share groups ("queues for Kafka") production-ready: many consumers share one partition and acknowledge records one at a time. That narrows the gap with classic job queues (Module 9).
1.7Kafka compared with other brokers
| Kafka | RabbitMQ | Amazon SQS | Apache Pulsar | |
|---|---|---|---|---|
| Model | Partitioned, replicated log | Broker with exchanges and queues | Fully managed queue | Log with storage in Apache BookKeeper |
| Retention and replay | Days, weeks, or forever; replay by offset | Deleted on ack (streams add replay) | 1 minute to 14 days, no replay | Configurable; replay by cursor |
| Ordering | Per partition | Per queue with a single consumer | FIFO queues: per message group | Per partition or key |
| Scaling consumers | Up to the partition count per group (share groups go beyond) | Competing consumers on a queue | Automatic | Shared and key-shared subscriptions |
| Delays and priorities | Not built in | Priorities; delays via plugin | Delay up to 15 minutes | Delayed delivery built in |
| You operate | Brokers and controllers (or use managed) | Broker cluster | Nothing | Brokers and BookKeeper |
1.8Common mistakes
Common mistakes
- Treating Kafka as a database: expecting lookups by key or field. Sink data into a store built for queries.
- Using Kafka for synchronous calls and then fighting timeouts with request-reply topics.
- Assuming a message disappears after it is read, then being surprised by replays and duplicates.
- Assuming one global order across a multi-partition topic.
- Sending large files through Kafka instead of a reference to object storage.
- Adding Kafka to a small system that one managed queue would serve.
1.9Self-check
- In one sentence, what is a log in Kafka's sense?
Reveal
An append-only, ordered sequence of records where each record has an offset, writers append to the end, and each reader tracks its own position.
- How does Kafka act as a queue and as pub/sub at the same time?
Reveal
Within one consumer group, partitions are split among members, so each record is processed by one member (queue). Different groups read independently, so each group gets every record (pub/sub).
- Why does putting Kafka between services reduce coupling?
Reveal
Producers write events without knowing who reads them, and consumers read at their own pace. Neither must be up, fast, or even exist at the same time as the other.
- Give two costs of moving from synchronous calls to events.
Reveal
Effects happen later (eventual consistency) and consumers must handle duplicates and retries; also, you operate another distributed system and debugging spans asynchronous hops.
- A product manager asks to "query Kafka for all transactions of user 7 last March". What do you say?
Reveal
Kafka is not indexed by user. Stream the topic into a database or warehouse (Kafka Connect makes this easy) and query there.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 2: Events, topics, partitions, and offsets
The four words you will use every day. A record is an event; a topic is a named stream of records; a partition is one ordered log inside a topic; an offset is a record's position in that log. Get these exact and the rest of Kafka follows.
- A record has no key. Which partition does it go to?
Reveal
The producer spreads keyless records across partitions (it fills a batch for one partition, then moves to another). There is no ordering between them.
- A topic has 4 partitions. Is there one global order across the topic?
Reveal
No. Order exists only within each partition.
- Are offsets numbered per topic or per partition?
Reveal
Per partition. Each partition has its own sequence starting at 0.
- Consumer A has read up to offset 100 in a partition. Can consumer B read offset 50 of the same partition?
Reveal
Yes. Readers are independent; each tracks its own position.
- Retention is 7 days and a consumer was down for 8 days. What happens when it returns?
Reveal
Records older than about 7 days were deleted, so its committed offset may now point before the start of the log. It jumps to the earliest or latest offset according to
auto.offset.reset, and the deleted records are lost to it.
2.1Events and records
Write the record you would publish when a card payment is authorized. What is the key? What goes in the value? Which headers would you add?
Reveal the answer
Key: the card ID (or account ID), the entity whose events must stay in order. Value: an event ID for deduplication, amount, currency, merchant, authorization code, and the time it happened. Headers: event type, schema version, trace ID, and source service. Headers carry metadata; they do not affect routing.
An event says "something happened". Kafka stores it as a record: key, value, headers, and a timestamp.
A printed receipt. Once printed it never changes; a refund is a new receipt, not an edit.
Records are immutable. You never update one in place; you append a new record.
- Key and value are bytes. Producers turn objects into bytes with serializers and consumers turn them back with deserializers (Module 15). The key is optional.
- Headers are key-value pairs of metadata, such as a trace ID or an event type.
- Timestamp: by default the producer sets it when it creates the record (
CreateTime). A topic can switch to broker time withmessage.timestamp.type=LogAppendTime. Since Kafka 4.0 the broker rejectsCreateTimetimestamps more than one hour in the future by default (message.timestamp.after.max.ms). - Size: the default broker limit is about 1 MB per record batch (
message.max.bytes). Large payloads belong in object storage (Module 15).
2.2Topics
Should payment events and refund events share one topic?
Reveal the answer
It depends on ordering. If a consumer must see a refund after the payment it reverses, put both in one topic keyed by account or payment ID, because order only exists within a partition of one topic. If no consumer needs that cross-type order, separate topics are simpler to understand and secure.
- A topic is a named stream:
payments,card.authorizations.v1. Many producers can write to it and many consumers can read it. - Topic configs override broker defaults per topic:
retention.ms,cleanup.policy,min.insync.replicas,max.message.bytes, and more. - Create topics explicitly with chosen partitions, replication, and retention. Brokers auto-create topics on first use by default (
auto.create.topics.enable=true); most production clusters turn that off so a typo does not create a topic with one partition and one replica. - Name topics consistently, for example
<domain>.<entity>.<event>.v<version>. Names are case-sensitive and cannot be changed later; you create a new topic and migrate.
2.3Partitions
A topic must take 50 MB/s and, on your hardware, one partition handles about 10 MB/s. What is the minimum partition count, and why do more partitions help?
Reveal the answer
At least 5, and more for headroom. Each partition is an independent log that can live on a different broker and be read by a different consumer, so partitions let writes, storage, and reads run in parallel.
A partition is one ordered, append-only log. A topic is a set of partitions.
Supermarket checkout lanes. More lanes serve more customers, each lane is served in order, and there is no order between lanes.
The partition is the unit of parallelism, ordering, storage, and replication.
- Writes only append to the end of a partition. The leader assigns the next offset.
- Each partition replica lives on one broker, so one partition's throughput and size are bounded by one machine. More partitions spread a topic across machines.
- Partition count is chosen at creation. You can increase it later but never decrease it, and increasing it remaps keys (Module 6).
- Consumers: within one consumer group, each partition is read by at most one member at a time, so the partition count caps a group's parallelism (Module 7).
2.4Keys and ordering
- The producer picks the partition from the key: by default
murmur2(key bytes)modulo the partition count (Module 6). Same key, same partition, same order. - No key means the producer spreads records across partitions with no ordering promise.
- Choose the key as the entity whose events must stay in order: account, card, order, or chat conversation. That gives per-entity order and lets different entities process in parallel.
Kafka's ordering guarantee is per partition. Choose the key so that everything that must stay in order shares a partition, and accept that different keys interleave.
2.5Offsets
In partition 2 your consumer processed every record up to offset 6, then committed offset 7 and restarted. Which record does it read first?
Reveal the answer
Offset 7. A committed offset is the next record to read, not the last one processed.
- An offset is a record's position within its partition. The leader assigns it on append; it only increases and is never reused.
- Offsets can have gaps. Compaction removes records, and transaction markers take up offsets (Module 14). Do not count records by subtracting offsets when exactness matters.
- Consumers own their position. A consumer group commits offsets to Kafka (the internal
__consumer_offsetstopic) so a restart resumes where it left off (Module 8). - Consumers can move: to the beginning, the end, a specific offset, or the first offset at or after a timestamp.
2.6Retention: how long records live
- With the default
cleanup.policy=delete, records are removed when they are older thanretention.ms(7 days by default) or when a partition exceedsretention.bytes(unlimited by default). - Reading never deletes. Deletion removes whole log segments, so data can stay a little longer than the retention setting (Module 11).
- Retention can be infinite (
retention.ms=-1) for event-sourced or audit topics; budget disk or use tiered storage (Module 11). cleanup.policy=compactkeeps the latest record per key instead (Module 13).
Choose retention from three questions: how long can a consumer be down and still recover? How far back will you replay? What do cost and compliance allow?
If a consumer falls further behind than retention, it silently loses records: its offset becomes invalid and it resets to the earliest or latest offset. Alert on consumer lag measured in time, not only in messages.
2.7Lab: your first topic
Start the single broker from Lab setup, then open a shell in it:
docker exec -it --workdir /opt/kafka/bin broker shCreate a topic with three partitions and look at it:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic payments \
--partitions 3 --replication-factor 1
./kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic paymentsStart a producer that reads key:value lines:
./kafka-console-producer.sh --bootstrap-server localhost:9092 --topic payments \
--reader-property parse.key=true --reader-property key.separator=:Type these six lines, then press Ctrl-C:
acct-5:deposit 100
acct-4:deposit 50
acct-5:withdraw 30
acct-1:deposit 70
acct-5:withdraw 10
acct-4:withdraw 20Read everything, printing partition, offset, and key:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic payments --from-beginning \
--formatter-property print.partition=true \
--formatter-property print.offset=true \
--formatter-property print.key=truePress Ctrl-C, then read only partition 0 starting at offset 1:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic payments \
--partition 0 --offset 1 \
--formatter-property print.offset=true --formatter-property print.key=trueCheck what you saw
acct-5lands in partition 0 at offsets 0, 1, 2 in the order you typed;acct-4in partition 1;acct-1in partition 2. The key explorer in Module 6 computes the same partitions.- With
--from-beginning, the consumer may print partitions in any interleaving, but within each partition the offsets are in order. - The last command prints
withdraw 30andwithdraw 10(offsets 1 and 2): you chose where to start. - Run the full read again: the records are still there. Reading did not delete them.
2.8Common mistakes
Common mistakes
- Expecting global order across a multi-partition topic.
- Picking a key that does not match the ordering need, for example keying by merchant when per-account order is what matters.
- Relying on broker auto-creation and ending up with a one-partition, one-replica topic in production.
- Treating the committed offset as the last processed record: it is the next one to read.
- Setting retention shorter than the longest consumer outage you must survive.
- Doing arithmetic on offsets and assuming they are contiguous.
2.9Self-check
- What is the difference between a topic and a partition?
Reveal
A topic is a named stream; a partition is one ordered, append-only log inside it. A topic has one or more partitions spread across brokers.
- Why is the partition, not the topic, the unit of ordering?
Reveal
Each partition is appended by one leader in one sequence. Different partitions are independent logs on different brokers with no shared sequence.
- Which fields does the producer set, and which does the broker assign?
Reveal
The producer sets key, value, headers, and (by default) the timestamp. The broker assigns the partition's next offset; the partition itself is chosen by the producer's partitioner.
- Can you decrease a topic's partition count?
Reveal
No. You can only increase it, and doing so changes which partition a key maps to. To shrink, create a new topic and migrate.
- A consumer committed offset 500 in partition 3. What does that mean?
Reveal
It has processed everything before offset 500 in partition 3, and on restart it reads offset 500 next.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 3: Brokers, clusters, and KRaft
Where partitions physically live and who decides who leads them. This module covers brokers and replicas, the KRaft controller quorum that replaced ZooKeeper, how clients discover the right broker, the listener settings that trip up every first Docker setup, and the internal topics you will see in any cluster.
- A topic has replication factor 3. How many replica failures can committed data survive?
Reveal
Two, as long as the surviving replica was in sync. Writes with
acks=allmay stop earlier, depending onmin.insync.replicas. - A producer sends a record for partition 2. Which broker receives it?
Reveal
The current leader of partition 2. Clients learn leaders from metadata and send directly to them.
- What did ZooKeeper do for Kafka, and what replaced it?
Reveal
It stored cluster metadata and elected the controller. KRaft, a quorum of Kafka controllers using the Raft protocol, replaced it; Kafka 4.0 removed ZooKeeper support.
bootstrap.serverslists one broker, but the cluster has 12. Does the client use all 12?Reveal
Yes. The bootstrap address is only for the first metadata request, which returns every broker. List two or three for redundancy.
- A laptop producer reaches Kafka in Docker, but sends fail with "unknown host kafka". What is wrong?
Reveal
The broker's advertised listener tells clients to connect to
kafka:9092, a name only the Docker network can resolve.
3.1Brokers and the cluster
Three brokers, a topic with 6 partitions and replication factor 1. One broker dies. What happens?
Reveal the answer
The two partitions stored on that broker go offline: their reads and writes fail until it returns, because no other broker has a copy. The other four partitions keep working. This is why production topics use replication factor 3.
A broker is one Kafka server process. It stores partition replicas on its disks and serves produce and fetch requests for them. A cluster is a group of brokers that share metadata.
Bank branches. Each ledger has its master copy at one branch and photocopies at two others. Customers go to the branch with the master copy.
- Each broker has a unique
node.id. A broker can host thousands of partition replicas. - Partitions and their leaders are spread across brokers so load is shared.
- Leaders do the work: producers write to the leader; consumers read from the leader by default. Followers fetch from the leader to stay in sync.
3.2Replication in brief
- Replication factor (RF): how many copies of each partition exist, counting the leader. RF 3 is the production norm.
- In-sync replicas (ISR): the replicas that are fully caught up with the leader. A follower that falls behind for longer than
replica.lag.time.max.ms(30 seconds by default) is removed from the ISR. - Failover: if a leader's broker dies, the controller picks a new leader from the ISR, so no committed record is lost.
- The production recipe is RF 3,
min.insync.replicas=2, and producers withacks=all. Module 5 explains each part; Module 12 covers replication internals.
| Replication factor | Copies | Typical use |
|---|---|---|
| 1 | One; a broker loss takes the partition offline and can lose data | Local development only |
| 2 | Survives one failure, but writes with min.insync.replicas=2 stop when one replica is down |
Rare |
| 3 | Survives one failure with writes continuing, two failures without losing committed data | Production default |
3.3The controller and KRaft
Why can't each broker decide by itself which replica of a partition is the leader?
Reveal the answer
Two brokers could each believe they are the leader (split brain) and accept conflicting writes. Leadership must come from one source of truth that every broker agrees on, which needs a consensus protocol.
Controllers manage cluster metadata: which brokers are alive, each partition's leader and ISR, and topic configs. In KRaft mode they agree on that metadata with the Raft consensus protocol.
An air traffic control tower. Planes fly themselves, but the tower assigns runways and every pilot follows the tower.
- Roles: each node runs as a
broker, acontroller, or both (process.roles=broker,controller, called combined mode, fine for development but not recommended for critical production). - Quorum size: run 3 or 5 controllers. A majority must be alive: 3 tolerate one failure, 5 tolerate two.
- Active controller: the Raft leader among the controllers. It writes every metadata change to the
__cluster_metadatalog; standbys replicate it and can take over immediately. - Brokers fetch the metadata log to keep their cache current and send heartbeats. A broker that misses heartbeats for
broker.session.timeout.ms(9 seconds by default) is fenced, and its partitions get new leaders.
ZooKeeper mode was removed in Kafka 4.0. A ZooKeeper-based cluster must first migrate to KRaft on a 3.x release (3.9 is the recommended bridge) before upgrading to 4.x. Interview answers about "ZooKeeper stores offsets" are a decade out of date: offsets have lived in the __consumer_offsets topic since the Java consumer arrived in Kafka 0.9.
3.4How clients find the right broker
bootstrap.serversis only for first contact. List two or three brokers, or one DNS name that resolves to several, so a single dead broker does not block startup.- Clients route themselves. The producer picks the partition and sends to its leader; there is no proxy in the data path. Every client must be able to reach every broker directly.
- Metadata refresh happens periodically (
metadata.max.age.ms, 5 minutes) and immediately after errors such asNOT_LEADER_OR_FOLLOWER. Clients retry these errors automatically.
3.5Listeners and advertised listeners
listeners: where the broker binds, one entry per named listener, for exampleINTERNAL://:19092,EXTERNAL://:9092,CONTROLLER://:9093.advertised.listeners: the address the broker hands to clients in metadata, per listener. It must be resolvable and reachable from the clients that use that listener.listener.security.protocol.mapmaps each name to PLAINTEXT, SSL, SASL_PLAINTEXT, or SASL_SSL.inter.broker.listener.nameandcontroller.listener.namessay which listener brokers and controllers use among themselves.
listeners=INTERNAL://:19092,EXTERNAL://:9092,CONTROLLER://:9093
advertised.listeners=INTERNAL://kafka:19092,EXTERNAL://localhost:9092
listener.security.protocol.map=INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT
inter.broker.listener.name=INTERNAL
controller.listener.names=CONTROLLERIn Kubernetes, external clients need a reachable address for each broker (a per-broker service or load balancer). One shared load balancer works for bootstrap only; after metadata, clients connect to individual brokers.
3.6Internal topics you will see
| Topic | Holds | Notes |
|---|---|---|
__consumer_offsets |
Committed offsets and group metadata | 50 partitions by default, compacted |
__transaction_state |
Transaction coordinator state | Used by transactional producers (Module 14) |
__share_group_state |
Share group delivery state | Created on first use of share groups |
__cluster_metadata |
KRaft metadata log | Lives on controllers; not a regular topic |
connect-configs, connect-offsets, connect-status |
Kafka Connect state | Names are configurable (Module 16) |
<app>-...-changelog, <app>-...-repartition |
Kafka Streams state and reshuffles | Created by the Streams app (Module 17) |
_schemas |
Confluent Schema Registry storage | Part of the ecosystem, not Apache Kafka |
Never write to or delete internal topics by hand.
3.7Lab: a three-broker cluster and a failure
Start the three-node cluster from Lab setup. Define a helper so commands stay short:
k() { tool=$1; shift; docker exec -it kafka-1 /opt/kafka/bin/$tool --bootstrap-server kafka-1:19092 "$@"; }Create a replicated topic and describe it:
k kafka-topics.sh --create --topic orders --partitions 6 --replication-factor 3
k kafka-topics.sh --describe --topic ordersStop one node, describe again, and list under-replicated partitions:
docker stop kafka-2
k kafka-topics.sh --describe --topic orders
k kafka-topics.sh --describe --under-replicated-partitionsBring it back, wait 30 seconds, then restore preferred leaders and check the controller quorum:
docker start kafka-2
k kafka-topics.sh --describe --topic orders
k kafka-leader-election.sh --election-type preferred --all-topic-partitions
k kafka-metadata-quorum.sh describe --statusCheck what you saw
- Before the stop, each partition shows
Replicas: a,b,candIsr:with all three nodes, and leaders are spread across nodes 1, 2, 3. - After
docker stop kafka-2, node 2 disappears from everyIsrlist, and partitions it led now show leader 1 or 3. The topic keeps working. - After the restart, node 2 rejoins the ISR once caught up, but it leads nothing until the preferred leader election (or the automatic rebalance,
auto.leader.rebalance.enable, which runs every 5 minutes by default) moves leadership back to the first replica in each list. describe --statusshows the controller leader, the voters, and the high watermark of the metadata log. These are combined nodes, so stopping one node also stopped one of three controllers; the quorum survived because two of three remained.
3.8Common mistakes
Common mistakes
- Replication factor 1 or 2 in production, or relying on auto-created topics that use
default.replication.factor=1. - Wrong
advertised.listeners: clients connect for bootstrap and then fail on every request. - A load balancer in front of brokers for data traffic. Clients must reach each broker directly.
- An even number of controllers: 4 controllers tolerate one failure, the same as 3.
- Combined broker-controller nodes in a critical production cluster, so a busy broker can starve the controller.
- Describing ZooKeeper's role in a modern cluster: Kafka 4.x has none.
3.9Self-check
- What does the active controller do?
Reveal
It is the Raft leader of the controller quorum. It records metadata changes (topics, partition leaders, ISR, configs, broker registrations) in the metadata log and decides leader elections; brokers follow that log.
- Why 3 or 5 controllers, never 4?
Reveal
Raft needs a majority. 4 nodes need 3 alive, tolerating one failure, the same as 3 nodes, while costing more and adding a vote to wait for.
- What happens to a partition's leadership when its leader's broker stops heartbeating?
Reveal
After
broker.session.timeout.ms, the controller fences the broker and elects a new leader from the partition's ISR, then publishes the change; clients refresh metadata and move. - Why must every client reach every broker?
Reveal
Clients send each request to the leader of the target partition, which can be any broker. Only bootstrap can go through a shared address.
- What is the difference between
listenersandadvertised.listeners?Reveal
listenersis where the broker binds and accepts connections.advertised.listenersis the address it tells clients to use; it must be reachable from those clients.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 4: The producer, end to end
What actually happens between producer.send() and a record sitting on a broker. You will learn the send path, batching with batch.size and linger.ms, compression, the timeouts that bound every send, which errors are retried for you, and how to write a producer you can trust in production.
- Does
producer.send()block until the broker has the record?Reveal
No. It appends the record to an in-memory batch and returns a
Futureimmediately. A background thread sends batches; the callback or future completes when the broker replies. - Should you create a new
KafkaProducerfor each HTTP request?Reveal
No. A producer is thread-safe and expensive to create (connections, buffers, metadata). Share one per application and close it on shutdown.
- What do
linger.msandbatch.sizecontrol?Reveal
When a batch is sent: as soon as it reaches
batch.sizebytes, or after waitinglinger.msfor more records, whichever comes first. - Your callback receives an exception. Has the producer already retried?
Reveal
For retriable errors, yes: it retries until
delivery.timeout.ms(2 minutes by default) runs out. An exception in the callback means retries are exhausted or the error cannot be retried. - Where does compression happen: producer, broker, or consumer?
Reveal
The producer compresses whole batches, the broker stores them compressed (with the default topic setting), and the consumer decompresses.
4.1What send() does
Your service calls send() 10,000 times per second for 3 partitions. If each call made its own network request to the broker, what would hurt? What would you do instead?
Reveal the answer
Ten thousand small requests per second means per-request overhead (headers, syscalls, acknowledgments) dominates and the broker's request threads saturate. Instead, buffer records per partition and send them in batches: one request can carry hundreds of records. That is exactly what the producer does.
send() does the cheap work on your thread (serialize, pick a partition, append to a batch) and returns. A background sender thread does the network work in batches.
A post office counter. You drop letters in the bin for each city and walk away; a van leaves for a city when its bin is full or on a timer, carrying many letters per trip.
send() does. Your thread runs interceptors, serializes the key and value, picks a partition, and appends the record to a per-partition batch in memory, then returns. A background sender thread drains ready batches, groups them by leader broker, sends one request per broker, and completes callbacks when responses arrive.- Interceptors (optional) can inspect or modify each record, for example to add tracing headers.
- Serializers turn the key and value into bytes. A serialization error is thrown from
send()itself. - The partitioner picks the partition (Module 6). This needs topic metadata; if it is missing,
send()waits up tomax.block.ms(60 seconds). - The record accumulator appends the record to the open batch for that partition. Memory for all batches is capped by
buffer.memory(32 MB); when it is full,send()blocks up tomax.block.msand then fails the record with aTimeoutException, reported to the callback and the returnedFuture. - The sender thread drains ready batches, groups them by the broker that leads each partition, and sends one produce request per broker.
- The response completes each record's
Futureand runs its callback, on the producer's I/O thread.
4.2Writing a producer
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class PaymentProducer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// acks=all and idempotence are already the defaults; stating them documents intent
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
props.put(ProducerConfig.LINGER_MS_CONFIG, 10);
props.put(ProducerConfig.COMPRESSION_TYPE_CONFIG, "lz4");
try (Producer<String, String> producer = new KafkaProducer<>(props)) {
ProducerRecord<String, String> record =
new ProducerRecord<>("payments", "acct-42", "{\"type\":\"DEBIT\",\"amt\":1250}");
record.headers().add("event-type", "PaymentDebited".getBytes());
producer.send(record, (metadata, exception) -> {
if (exception != null) {
// retries are exhausted or the error is not retriable
System.err.println("send failed: " + exception);
} else {
System.out.printf("stored in %s-%d at offset %d%n",
metadata.topic(), metadata.partition(), metadata.offset());
}
});
} // close() sends everything still buffered and waits for responses
}
}- Asynchronous with a callback (above) is the normal style. Keep callbacks short: they run on the producer's only I/O thread, so a slow callback delays every other send. Never call
flush()inside a callback (Kafka 4.1+ rejects it). - Synchronous:
producer.send(record).get()blocks until the broker answers. It is simple and correct, but it sends one record per round trip. Use it in scripts, tests, or when you must confirm before replying to a caller, and expect much lower throughput. - One producer per application, shared across threads. Sharing also helps batching.
flush()sends everything buffered and waits. Use it when you must be sure records are written before a next step, such as committing a consumer offset in a non-transactional pipeline.
4.3Batching: batch.size and linger.ms
A producer sends 200 records per second, about 1 KB each, to one partition. With linger.ms=0, roughly how many records go in each request? With linger.ms=20?
Reveal the answer
At 200 records per second, records arrive about every 5 ms. With linger.ms=0, a batch is sent as soon as the sender is free, so most requests carry one record. With linger.ms=20, the producer waits up to 20 ms, so a batch collects about 4 records: four times fewer requests for at most 20 ms of extra latency.
batch.size(16 KB by default) is the maximum size of one partition's batch. A full batch is sent at once.linger.ms(5 ms by default since Kafka 4.0, previously 0) is how long a batch may wait for more records before it is sent anyway.- Under heavy load batches fill before
linger.msexpires, so linger adds no latency. Under light load a small linger groups records that would otherwise go one per request. - Typical high-throughput settings:
batch.size64 KB to 256 KB andlinger.ms10 to 50 ms. Measure; Module 19 covers tuning.
4.4Compression
compression.type |
Ratio | CPU | When to use |
|---|---|---|---|
none (default) |
1x | None | Already compressed or tiny payloads |
lz4 |
Good | Very low | Common default choice for throughput |
zstd |
Best | Low to moderate | Large JSON or text, network or disk bound |
snappy |
Moderate | Low | Older setups |
gzip |
High | High | Rarely the best choice today |
- Compression works on whole batches, so bigger batches compress better: batching and compression reinforce each other.
- The broker keeps the producer's compression when the topic's
compression.typeisproducer(the default). If a topic forces a different codec, the broker must recompress every batch, which costs broker CPU. - Consumers decompress automatically; nothing to configure.
4.5Timeouts and errors
A leader election takes 8 seconds. The producer has delivery.timeout.ms=120000. What does the application see?
Reveal the answer
Nothing, apart from some latency. The producer gets NOT_LEADER_OR_FOLLOWER or timeouts, refreshes metadata, and retries on the new leader. As long as the record succeeds within 120 seconds, the callback reports success.
delivery.timeout.ms(120 s) is the upper bound on how long a record may take fromsend()returning to its callback: time waiting in the batch, every retry, and time in flight. When it expires the callback gets aTimeoutException.request.timeout.ms(30 s) bounds one request to the broker.retriesdefaults to the maximum integer: in practicedelivery.timeout.msdecides when to give up. Retries back off fromretry.backoff.ms(100 ms) up toretry.backoff.max.ms(1 s).
| Error | Retriable? | What it means |
|---|---|---|
NotLeaderOrFollowerException |
Yes | Leadership moved; metadata refresh fixes it |
NotEnoughReplicasException |
Yes | Fewer in-sync replicas than min.insync.replicas (Module 5) |
TimeoutException (request) |
Yes | Broker slow or unreachable |
UnknownTopicOrPartitionException |
Yes | Topic missing or metadata stale |
RecordTooLargeException |
No | Record bigger than max.request.size or the topic's limit |
SerializationException |
No | Thrown by send() itself |
TopicAuthorizationException |
No | The client lacks permission |
When the callback reports a failure, the record is not in Kafka. Decide deliberately: fail the user's request, keep the event in a database outbox and publish later (Module 14), or alert. Logging and moving on silently loses data.
4.6Lab: measure batching and compression
In the single-broker container shell, create a topic and run the producer load test three times:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic perf --partitions 6 --replication-factor 1
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic perf \
--num-records 500000 --record-size 1000 --throughput -1 \
--command-property linger.ms=0 --command-property batch.size=16384
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic perf \
--num-records 500000 --record-size 1000 --throughput -1 \
--command-property linger.ms=20 --command-property batch.size=131072
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic perf \
--num-records 500000 --record-size 1000 --throughput -1 \
--command-property linger.ms=20 --command-property batch.size=131072 \
--command-property compression.type=lz4Compare the final line of each run: records/sec, MB/sec, and the average and 99th percentile latency.
Check what you saw
- The second run usually shows clearly higher throughput than the first: larger, fuller batches mean fewer requests.
- The test sends random bytes, which barely compress, so lz4 changes little here. With real JSON payloads, compression often shrinks data several times and raises throughput further.
- Latency numbers include time spent waiting in batches, so a larger linger can raise average latency even while throughput improves. Under this full load the batches fill quickly, so the effect is small.
- Your exact numbers depend on your laptop. The point is the direction of each change.
4.7Common mistakes
Common mistakes
- Creating a producer per request or per message, which wastes connections and kills batching.
- Calling
.get()on every send in a hot path, turning a batching client into one record per round trip. - Ignoring the callback's exception, so failed sends vanish silently.
- Doing slow work in the callback, which stalls the producer's I/O thread.
- Never closing the producer, which drops buffered records on shutdown.
- Setting
linger.ms=0everywhere out of habit and getting tiny batches under normal load.
4.8Self-check
- List the steps a record takes inside the producer before it leaves the machine.
Reveal
Interceptors, key and value serialization, partition selection, append to that partition's batch in the record accumulator, then the sender thread drains ready batches into one request per leader broker.
- When is a batch sent?
Reveal
When it reaches
batch.size, when it has waitedlinger.ms, or when something forces it out (flush(),close(), or a full buffer). - What happens when
buffer.memoryis exhausted?Reveal
send()blocks for up tomax.block.ms, then fails the record with aTimeoutException(through the callback andFuture). It means the producer cannot send as fast as you produce, often because brokers are slow or unreachable. Becausesend()can block, lowermax.block.msin threads that must never stall, such as an event loop. - Which timeout decides when the producer gives up on a record?
Reveal
delivery.timeout.ms, which covers batching, all retries, and in-flight time. - Why does compression get better with larger batches?
Reveal
The codec compresses a whole batch at once, so it finds more repetition across many similar records.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 5: Durability and ordering on the write path
When can a producer trust that a record is safe, and will records stay in order? This module covers acks, min.insync.replicas, why retries create duplicates, how the idempotent producer removes them, how in-flight requests interact with ordering, and the safe producer configuration you should be able to recite.
- With
acks=1, when does the producer get its acknowledgment?Reveal
As soon as the leader has written the record to its own log, before any follower has copied it.
- Replication factor 3,
min.insync.replicas=2,acks=all. Two brokers holding replicas go down. Can you still write?Reveal
No. Only one replica is in sync, fewer than 2, so writes fail with
NotEnoughReplicasException. Reads still work and no acknowledged record is lost. - Why can a producer retry create a duplicate?
Reveal
The broker may have written the batch and only the acknowledgment was lost. The producer cannot tell, so it sends the batch again.
- What does
enable.idempotence=trueadd to each batch?Reveal
A producer ID and a per-partition sequence number, so the broker can recognize and drop a batch it has already written.
- You set
acks=1and leaveenable.idempotenceunset. Is the producer idempotent?Reveal
No. Idempotence needs
acks=all; when a conflicting setting is present and idempotence was not set explicitly, the client silently disables it.
5.1acks: when the leader answers
For each, choose acks=0, 1, or all: (a) debug logs you can afford to lose, (b) clickstream for a dashboard, (c) card authorizations feeding the ledger.
Reveal the answer
(a) acks=0 or 1: loss is acceptable and latency is lowest. (b) acks=1 is common, though all costs little with batching. (c) acks=all with min.insync.replicas=2: an acknowledged authorization must survive a broker crash.
acks settings. The setting decides when the leader answers the producer: never (0), after its own write (1), or after every in-sync replica has the record (all, the default since Kafka 3.0).acks |
Producer waits for | Can an acknowledged record be lost? | Latency |
|---|---|---|---|
0 |
Nothing | Yes, and failures are not even reported | Lowest |
1 |
The leader's own write | Yes, if the leader dies before followers copy it | Low |
all (or -1) |
Every in-sync replica | No, while one in-sync replica survives and unclean election is off | Higher by one replication round trip |
acks=all has been the default since Kafka 3.0. "All" means all replicas currently in the ISR, not all replicas that exist. That is why it needs a floor, which is the next lesson.
5.2min.insync.replicas: a floor for acks=all
Replication factor 3, acks=all, and min.insync.replicas=1. Two followers fall out of the ISR. The leader acknowledges a write and then its disk dies. What happened?
Reveal the answer
With only the leader in the ISR, "all in-sync replicas" was just the leader, so the write was acknowledged with a single copy, and that copy is gone. min.insync.replicas=2 would have refused the write instead of pretending it was safe.
min.insync.replicasis a topic config (with a broker or cluster default) and only matters for producers usingacks=all.- If the ISR is smaller than this number, the leader rejects the write with
NotEnoughReplicasException. If the ISR shrinks after the leader appended but before the acknowledgment, the producer getsNotEnoughReplicasAfterAppendException. Both are retriable, so the producer keeps trying untildelivery.timeout.ms. - The standard recipe is replication factor 3 with
min.insync.replicas=2: writes continue with one broker down, and every acknowledged record exists on at least two brokers.
| RF, min ISR | Brokers down | acks=all writes |
Acknowledged data |
|---|---|---|---|
| 3, 2 | 0 or 1 | Accepted | Safe |
| 3, 2 | 2 | Rejected (NotEnoughReplicas) |
Safe; reads still served |
| 3, 1 | 2 | Accepted on the leader alone | At risk if the leader then fails |
| 3, 3 | 1 | Rejected | Safe, but any single failure stops writes |
Since Kafka 3.7, a leader also stops advancing the high watermark while the ISR is smaller than min.insync.replicas. Records written with acks=1 in that state are accepted but stay invisible to consumers until the ISR recovers. Module 12 explains the high watermark.
5.3Retries and the duplicate problem
- A timeout is ambiguous: the request may have failed, or it may have succeeded with the response lost. The producer must retry to avoid loss, and retries risk duplicates.
- At-least-once is what you get from retries alone: nothing lost, duplicates possible.
- The idempotent producer removes the duplicates that its own retries create.
5.4The idempotent producer
Each producer gets a producer ID (PID) from the broker and numbers its batches per partition. The leader remembers the last few sequence numbers per PID and partition and rejects anything it has already written.
Numbered cheques. A bank that has cleared cheque 107 from your account will not clear another cheque 107, even if the courier delivers it twice.
- On by default since Kafka 3.0. It requires
acks=all,retriesabove 0, andmax.in.flight.requests.per.connectionof 5 or less. If you setacksto 0 or 1, orretries=0, without explicitly enabling idempotence, the client quietly turns idempotence off (withenable.idempotence=trueset explicitly, it refuses to start instead). An in-flight limit above 5 fails at startup unless you explicitly disable idempotence. - Cost: almost none. Keep it on.
- Scope: one producer instance (one PID), per partition. It does not protect against:
- your own code catching a failed send and calling
send()again (a new record with a new sequence number), - a producer that restarts and resends records it was unsure about (a new PID),
- two producer instances sending the same business event.
- your own code catching a failed send and calling
- For those cases you need transactions (a stable
transactional.id, Module 14) or idempotent consumers keyed by a business ID (Module 10).
5.5Ordering with retries in flight
max.in.flight.requests.per.connection=5 lets the producer send 5 requests to a broker before hearing back. Without idempotence, how can that reorder records within one partition?
Reveal the answer
Request 1 fails with a retriable error while request 2 succeeds. The producer retries request 1, which lands after request 2. The partition now holds batch 2 before batch 1.
- With idempotence on, the broker requires sequence numbers to arrive in order. Batch 2 is rejected until batch 1 is written, so ordering holds with up to 5 requests in flight.
- With idempotence off, the only way to guarantee order with retries is
max.in.flight.requests.per.connection=1, which costs throughput. This was common advice before Kafka 3.0.
5.6The safe producer configuration
# durability: wait for every in-sync replica (default since 3.0)
acks=all
# no duplicates from the producer's own retries, ordering kept (default since 3.0)
enable.idempotence=true
# keep retrying transient errors for up to 2 minutes (defaults)
retries=2147483647
delivery.timeout.ms=120000
# up to 5 is safe with idempotence (default)
max.in.flight.requests.per.connection=5replication.factor=3
min.insync.replicas=2
unclean.leader.election.enable=falseAnd in your code: handle the callback's exception. A record whose callback reports failure is not in Kafka, whatever the settings say.
"How do you make sure a payment event is never lost and never duplicated by the producer?" A strong answer names all of it: acks=all, replication factor 3, min.insync.replicas=2, unclean election off, idempotence on, callbacks that handle failure, and for duplicates beyond one producer session, a business ID that consumers deduplicate on.
5.7Lab: min.insync.replicas in action
Use the three-broker cluster and the k helper from Lab setup. To see the effect with only one node down (so the controller quorum keeps its majority), set the floor to 3 for this experiment:
k kafka-topics.sh --create --topic ledger --partitions 1 --replication-factor 3 \
--config min.insync.replicas=3
k kafka-topics.sh --describe --topic ledgerNote the leader, then stop one of the other two nodes. For example, if the leader is 1:
docker stop kafka-3
k kafka-topics.sh --describe --topic ledgerTry a write with acks=all, then with acks=1. Type one line in each, then press Ctrl-C. (retries=0 is only there so the first error shows at once instead of after two minutes of retries.)
docker exec -it kafka-1 /opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server kafka-1:19092 --topic ledger \
--command-property acks=all --command-property retries=0
docker exec -it kafka-1 /opt/kafka/bin/kafka-console-producer.sh \
--bootstrap-server kafka-1:19092 --topic ledger --command-property acks=1Restart the node when you are done: docker start kafka-3.
Check what you saw
- After the stop,
describeshows an ISR of two nodes, below the topic'smin.insync.replicas=3. - The
acks=allproducer reports aNotEnoughReplicasException: the leader refuses a write it cannot place on enough replicas. With normal retries the producer would keep trying untildelivery.timeout.ms. No acknowledged data was put at risk. - The
acks=1producer reports no error: the leader accepted the record with fewer copies than the topic demands. Because the ISR is belowmin.insync.replicas, the high watermark does not advance, so consumers will not see this record until the third replica rejoins. - With the production setting
min.insync.replicas=2, the same thing happens when two of three replicas are unavailable.
5.8Common mistakes
Common mistakes
acks=allwithmin.insync.replicas=1: "all" can mean just the leader.min.insync.replicasequal to the replication factor: one broker restart stops all writes.- Turning off idempotence by setting
acks=1without noticing. - Believing idempotence gives end-to-end exactly-once: it only covers one producer's own retries.
- Retrying failed sends in application code without a business ID for deduplication.
- Raising
max.in.flight.requests.per.connectionabove 5, which is incompatible with idempotence (Kafka 4.0 no longer falls back silently).
5.9Self-check
- What exactly does
acks=allwait for?Reveal
For the record to be written by every replica that is in the partition's ISR at that moment, and only if the ISR has at least
min.insync.replicasmembers. - Why does the standard setup use RF 3 and
min.insync.replicas=2rather than 3 and 3?Reveal
With 2, writes continue through one broker failure and every acknowledged record is on two brokers. With 3, any single broker restart blocks all writes.
- How does the broker detect a duplicate batch from an idempotent producer?
Reveal
It tracks the producer ID, epoch, and the last sequence numbers it wrote for each partition. A batch whose sequence it has already written is acknowledged without being written again.
- Why is
max.in.flight.requests.per.connectionlimited to 5 with idempotence?Reveal
The broker keeps metadata for the last 5 batches per producer and partition to detect duplicates and enforce order; more in-flight batches than that could not be checked.
- Name two duplicate sources the idempotent producer does not prevent.
Reveal
Application code that re-sends after a failure, and a restarted producer that resends records it was unsure about. Each produces records with new sequence numbers or a new producer ID.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 6: Keys and partitioning
The key you choose decides ordering, load balance, and how far a topic can scale. This module shows exactly how the producer picks a partition, how to choose a key, how to spot and fix hot partitions, what changes when you add partitions, and how to size the partition count.
- Two records have the same key. Will they always land in the same partition?
Reveal
Yes, as long as the partition count does not change and both producers hash the same serialized key bytes the same way.
- Why might a Java producer and a Python (librdkafka) producer put the same key in different partitions?
Reveal
librdkafka's default partitioner hashes keys with CRC32, while the Java client uses murmur2. Set librdkafka's
partitioner=murmur2_randomto match Java. - One merchant produces 60% of all payment events and the key is
merchant_id. Will adding partitions help?Reveal
No. One key always maps to one partition, so that partition, its broker, and its consumer still carry 60% of the load. Change or extend the key.
- You grow a keyed topic from 6 to 8 partitions. What breaks?
Reveal
Most keys now hash to a different partition. A key's old records stay in the old partition and its new records go to the new one, so a consumer can process new events before old ones.
- What caps how many consumers in one group can work in parallel?
Reveal
The number of partitions. Extra consumers sit idle (share groups are the exception).
6.1How the producer picks a partition
- Keyed records:
partition = toPositive(murmur2(serialized key bytes)) % numPartitions. Same bytes, same partition. - Keyless records: the producer sticks to one partition until about
batch.sizebytes have gone to it, then switches. (Sticky partitioning arrived in Kafka 2.4; the current byte-based, adaptive version in 3.3. Before that, keyless records went round robin.) This builds full batches instead of many tiny ones. Withpartitioner.adaptive.partitioning.enable=true(the default) it sends more to partitions whose brokers are keeping up. - The hash runs on bytes, not on your object. Changing the key serializer (for example from String to Avro) changes the partition for the same logical key.
partitioner.ignore.keys=truemakes keyed records use the sticky behavior too. That gives up per-key ordering.
6.2Choosing a key
Pick a key for each topic: (a) card authorizations where a card's events must be processed in order, (b) app click events for analytics where order does not matter, (c) chat messages that must appear in order per conversation, (d) inventory updates per SKU in a flash sale with one viral product.
Reveal the answer
(a) card_id. (b) No key, or a random one: maximum spread. (c) conversation_id. (d) sku_id gives per-SKU order, but the viral SKU becomes a hot key; see the next lesson for options.
A good key has three properties:
- It matches the ordering need. Everything that must be processed in order shares the key, and nothing more.
- It has many distinct values, far more than the partition count, so load spreads evenly.
- It has no giants. No single value should carry a large share of traffic.
| Need | Key | Notes |
|---|---|---|
| Per-entity order (account, card, order, user) | That entity's ID | The common case |
| Per-tenant order in a multi-tenant system | tenant_id |
Watch for one huge tenant |
| No ordering need | null |
Best spread, best batching |
| Joins in stream processing | The join key | Both topics need the same key and partition count (Module 17) |
6.3Hot partitions
- Symptoms: one partition's consumer lag grows while others are near zero; one broker has much higher bytes in; one consumer instance is at 100% CPU.
- More partitions do not help a single hot key. All of that key's records still map to one partition.
- Fixes, from least to most invasive:
- Pick a finer key that still meets the ordering need, such as
card_idinstead ofmerchant_id. - Salt the hot key:
merchant-42#0tomerchant-42#7spreads one key over 8 partitions. You lose per-merchant order, so only do this when downstream can reorder or does not care (such as counting). - Route known giants to a dedicated topic with its own scaling.
- Speed up per-record processing on the hot partition, for example by batching writes downstream.
- Pick a finer key that still meets the ordering need, such as
6.4The key-to-partition explorer
Type any key to see the partition the Java producer chooses, then simulate many keys and one hot key.
6.5Changing the partition count
hash mod N for most keys. Records already written stay where they are, so a key's old records sit in one partition and its new records in another, and per-key order across the change is lost.- You can only add partitions, with
kafka-topics.sh --alter --partitions N. Existing records never move. - Keyed topics change mapping: after the change, many keys go to different partitions. Old records of a key stay behind, so during the transition a consumer may handle a key's new records before its old ones.
- Compacted topics are worse: a key's old value sits in one partition and its new value in another, and compaction never reconciles them. Readers that rebuild a table from the topic can end up with a stale value.
- Stream processing joins need both inputs to have the same partition count; changing one breaks the join (Module 17).
- Safe ways to grow a keyed topic:
- Size it for years of growth up front; partitions are cheap compared with a migration.
- Create a new topic with more partitions, dual-write or replay into it, and move consumers over once they have caught up.
- Drain first: stop producers, let consumers finish, add partitions, then resume. Order across the change is then preserved.
6.6How many partitions
Peak is 30,000 messages per second, 1 KB each. One consumer instance processes 2,000 messages per second. A partition comfortably absorbs 10 MB/s of writes on your brokers. How many partitions, with 2x headroom?
Reveal the answer
Consumers need 30,000 / 2,000 = 15. Writes need 30 MB/s / 10 MB/s = 3. The larger is 15; with 2x headroom, 30. Rounding to a number that divides evenly across brokers and consumer counts, 32 or 36 is a sensible choice.
Too few partitions cap consumer parallelism and per-topic throughput. Too many cost something too:
- Each partition replica means open files and memory on its broker, and work for the controller during failures and reassignments.
- Producers keep a batch per partition, so many partitions mean smaller, emptier batches for the same traffic, which lowers compression and raises request counts.
- Consumers get more partitions to fetch and more to move in a rebalance.
KRaft handles far more partitions per cluster than the ZooKeeper era did, but the batching cost is unchanged. Most topics are fine with tens of partitions; go into the hundreds only when the throughput math says so.
6.7Custom partitioners
A custom partitioner is rarely needed. Valid uses: give one huge tenant dedicated partitions, or keep a region's data in partitions whose leaders sit in that region's racks.
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.utils.Utils;
import java.util.Map;
public class TenantPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
int n = cluster.partitionCountForTopic(topic);
if ("tenant-big".equals(key)) {
return 0; // reserved for the giant tenant
}
return 1 + Utils.toPositive(Utils.murmur2(keyBytes)) % (n - 1);
}
@Override public void configure(Map<String, ?> configs) { }
@Override public void close() { }
}Register it with partitioner.class. Every producer writing to the topic must use the same partitioner, or keys scatter.
6.8Lab: predict partitions, then add one
On the single broker, create a topic and produce nine keys:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic accounts --partitions 3 --replication-factor 1
./kafka-console-producer.sh --bootstrap-server localhost:9092 --topic accounts \
--reader-property parse.key=true --reader-property key.separator=:acct-1:a
acct-2:a
acct-3:a
acct-4:a
acct-5:a
acct-6:a
acct-7:a
acct-8:a
acct-9:aBefore reading them back, predict each key's partition with the explorer above (3 partitions). Then check:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic accounts --from-beginning \
--formatter-property print.partition=true --formatter-property print.key=trueNow grow the topic to 4 partitions. Then run the same producer command, type the nine keys again with value b (acct-1:b and so on), and read everything again:
./kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic accounts --partitions 4Check what you saw
- With 3 partitions:
acct-5,acct-6,acct-9in partition 0;acct-4,acct-7,acct-8in partition 1;acct-1,acct-2,acct-3in partition 2. These match the explorer because it runs the same murmur2. - With 4 partitions the
brecords go to:acct-4,acct-5,acct-7in 0;acct-6,acct-8in 1;acct-1,acct-9in 2;acct-2,acct-3in 3. acct-6'sais in partition 0 and itsbin partition 1. A consumer reading partition 1 before partition 0 seesbbeforea: the ordering break, made visible.
6.9Common mistakes
Common mistakes
- Keying by a low-cardinality field (country, status, merchant) and creating hot partitions.
- Adding partitions to a keyed or compacted topic in place without planning for the remapping.
- Mixing producers with different partitioners (Java murmur2 and librdkafka's default CRC32) on one keyed topic.
- Changing the key serializer and silently moving every key.
- Expecting more partitions to fix a single hot key.
- Choosing hundreds of partitions "just in case" for a low-traffic topic, then getting tiny batches.
6.10Self-check
- Write the formula the Java producer uses for a keyed record.
Reveal
toPositive(murmur2(keyBytes)) % numPartitions, wherekeyBytesis the serialized key. - What does the producer do with keyless records, and why?
Reveal
It sticks to one partition until roughly a batch worth of data has gone there, then switches (adaptively favoring faster brokers). This builds full batches instead of scattering tiny ones.
- Name three properties of a good key.
Reveal
It matches the ordering requirement, it has many distinct values, and no single value carries a large share of traffic.
- How do you grow a keyed topic without breaking per-key order?
Reveal
Either drain it first (stop producers, let consumers finish, then add partitions), or create a new, larger topic and migrate producers and consumers to it.
- Give the partition count formula from throughput.
Reveal
The larger of peak rate divided by one consumer's rate and peak MB/s divided by one partition's write MB/s, multiplied by a growth factor.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 7: The consumer and consumer groups
Consumers pull records in a loop you write, and consumer groups spread partitions across instances. This module covers the poll loop, how groups divide partitions, subscribe versus assign, where a new group starts reading, the fetch settings, and how to add threads without breaking ordering.
- Does Kafka push records to consumers?
Reveal
No. Consumers pull by calling
poll(), which lets each consumer go at its own pace and fetch in large batches. - Can two threads share one
KafkaConsumer?Reveal
No. The consumer is not thread-safe. Use one consumer per thread; only
wakeup()may be called from another thread. - A new consumer group subscribes to a topic with a week of data. Where does it start with default settings?
Reveal
At the end (
auto.offset.reset=latest), so it sees only new records. Setearliestto read the history. - Two consumer groups read the same topic. Do they compete for records?
Reveal
No. Each group gets every record and keeps its own committed offsets.
- A group has 3 consumers and the topic has 12 partitions. How many partitions does each consumer get?
Reveal
About 4 each. Partitions are spread across the members, and each partition belongs to exactly one member.
7.1Pull, not push
Why might a pull model suit Kafka better than having brokers push records to consumers?
Reveal the answer
Each consumer controls its own pace, so a slow consumer does not need the broker to track and throttle it. Consumers fetch large batches when they are ready, which is efficient, and replay is natural: a consumer simply fetches from an older offset.
- Consumers send fetch requests to partition leaders. Long polling avoids busy loops: the broker holds a fetch until
fetch.min.bytesof data is available (1 byte by default) orfetch.max.wait.mspasses (500 ms). - The client prefetches in the background, so
poll()usually returns records already in memory.
7.2The poll loop
poll() returns a batch of records already fetched (and starts the next fetch); you process them, commit progress, and poll again. poll() is also where rebalances happen and where automatic commits are sent, so the loop must keep turning.import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.time.Duration;
import java.util.List;
import java.util.Properties;
public class BillingConsumer {
public static void main(String[] args) {
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG, "billing");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) {
consumer.subscribe(List.of("payments"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> r : records) {
System.out.printf("p%d o%d %s=%s%n", r.partition(), r.offset(), r.key(), r.value());
}
consumer.commitSync(); // commit what this poll returned, after processing it
}
}
}
}poll(Duration)returns up tomax.poll.records(500) records, possibly from several partitions; records from one partition arrive in offset order.- Process, then commit, then poll again. Module 8 covers commit strategies; Module 10 covers clean shutdown with
wakeup(). - Keep the loop moving. If the time between two
poll()calls exceedsmax.poll.interval.ms(5 minutes), the consumer is considered stuck and leaves the group (Module 9).
7.3Consumer groups
A topic has 4 partitions. Your group runs 6 instances to "go faster". What happens, and what would actually help?
Reveal the answer
Four instances each get one partition and two sit idle. To use 6 instances, the topic needs at least 6 partitions, or each record's processing must get faster. (Share groups, Module 9, can use more consumers than partitions when per-key order does not matter.)
- All consumers with the same
group.idform one group. Kafka assigns each partition to exactly one member, so members never process the same record at the same time. - Groups are independent: billing, fraud, and analytics each get every record.
- When members join or leave, partitions move (a rebalance, Module 9).
- Parallelism = min(partitions, consumers) for a classic or KIP-848 consumer group.
7.4subscribe or assign
subscribe(topics) |
assign(partitions) |
|
|---|---|---|
| Who picks partitions | The group protocol | You |
| Rebalancing | Automatic when members change | None |
| Committed offsets | Stored per group | Optional (you may still commit with a group.id) |
| Used by | Almost every application | Stream processors that manage their own state (Flink), tools, replay jobs |
subscribe(Pattern)follows topics that match a regular expression, including new ones as they appear.- Do not mix
subscribeandassignon one consumer.
7.5Where a group starts reading
auto.offset.reset applies only when the group has no committed offset for a partition, or its committed offset no longer exists (deleted by retention):
| Value | Starts at | Typical use |
|---|---|---|
latest (default) |
The end: only new records | Notifications where history is irrelevant |
earliest |
The oldest retained record | Anything that must not miss data: ledgers, sinks, backfills |
by_duration:PT6H (Kafka 4.0+) |
The first record from that long ago | Start a new service with recent history |
none |
Throws an exception | You want a human to decide |
A new group with the default latest silently skips everything produced before it first connected, and so does an existing group whose offsets expired. For most backend consumers, set earliest explicitly.
7.6Fetch settings
| Setting | Default | Effect |
|---|---|---|
fetch.min.bytes |
1 | Broker waits for this much data before answering; raise it for throughput |
fetch.max.wait.ms |
500 | Upper bound on that wait |
max.partition.fetch.bytes |
1 MB | Max data per partition per fetch |
fetch.max.bytes |
50 MB | Max data per fetch response |
max.poll.records |
500 | Max records one poll() returns; lower it if processing is slow |
max.poll.interval.ms |
300,000 (5 min) | Max time between polls before the member is removed |
7.7Threads and ordering
- Model A, one consumer per thread: run N consumers in the same group (in one process or many). Simple, ordering per partition is automatic, and rebalancing handles failures. Limited by the partition count.
- Model B, a poller plus workers: one thread polls and hands records to a pool. More parallelism than partitions, but you must (1) send each key to the same worker to keep per-key order, (2) commit an offset only when every earlier record of that partition is finished, and (3) pause partitions when worker queues fill. Libraries such as Confluent's Parallel Consumer implement this pattern.
- Share groups (Module 9) are the built-in alternative when per-key order does not matter.
7.8Lab: a consumer group in action
On the single broker, create a topic with 3 partitions and produce some records:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic orders --partitions 3 --replication-factor 1
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic orders \
--num-records 3000 --record-size 100 --throughput 100While that runs (about 30 seconds), open three more terminals, enter the container in each (docker exec -it --workdir /opt/kafka/bin broker sh), and start one consumer per terminal in the same group:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group billing \
--formatter-property print.partition=trueIn the first terminal, describe the group:
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billing
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billing --membersStart a fourth consumer in a fifth terminal and describe the group again. Then stop one consumer with Ctrl-C and describe once more.
Check what you saw
- With three consumers,
--describelists each partition with a differentCONSUMER-ID. Each terminal prints lines starting with only its ownPartition:number. - With four consumers,
--membersshows one member with#PARTITIONS0: it is idle. - When you stop one, its partition moves to another member within seconds, and that terminal starts printing two partition numbers.
- The
LAGcolumn shows how far each partition's committed offset is behind its end. Module 8 explains it.
7.9Common mistakes
Common mistakes
- Sharing one
KafkaConsumeracross threads. - Running more consumers than partitions and expecting more throughput.
- Relying on the default
auto.offset.reset=latestfor a consumer that must not miss data. - Slow work inside the poll loop that pushes the gap between polls past
max.poll.interval.ms. - Handing records to a thread pool without keeping per-key order or tracking which offsets are safe to commit.
- Reusing a
group.idacross unrelated applications, so they steal partitions from each other.
7.10Self-check
- What does
poll()do besides returning records?Reveal
It sends fetch requests, runs rebalance callbacks, sends automatic commits when they are due, and keeps the member in the group. The loop must keep calling it.
- Why can a consumer group not use more consumers than partitions?
Reveal
Each partition is assigned to exactly one member of a classic or KIP-848 consumer group, so there are only as many units of work as partitions.
- When does
auto.offset.resetapply?Reveal
Only when the group has no committed offset for a partition or that offset is no longer in the log.
- When would you use
assign()instead ofsubscribe()?Reveal
When your application controls which partitions it reads and stores its own position, such as a stream processor with checkpointed offsets, a replay tool, or a job that reads one specific partition.
- Name two things you must handle if you process one partition's records in a worker pool.
Reveal
Per-key ordering (route a key to one worker) and safe commits (commit only offsets whose earlier records are all done); also backpressure, by pausing partitions when queues fill.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 8: Offsets, commits, and delivery semantics
A consumer's committed offset decides what happens after every crash, deploy, and rebalance. This module separates the four positions in a partition, compares automatic and manual commits, shows how commit order produces at-most-once or at-least-once delivery, and covers resetting offsets to replay data.
- Your consumer processed offset 41 in a partition. Which offset should it commit?
Reveal
42, the next record to read. Committing 41 would reprocess offset 41 after a restart.
- Where are committed offsets stored?
Reveal
In the internal
__consumer_offsetstopic, keyed by group, topic, and partition. - With auto-commit on, can you lose records? Can you get duplicates?
Reveal
Duplicates, yes: up to a commit interval's worth after a crash. Loss, only if you hand records to other threads and the commit happens before they finish.
- What is consumer lag?
Reveal
For each partition, the distance between the end of what consumers can read (the high watermark) and the group's committed offset.
- A group was stopped for 10 days with default broker settings. What happens to its offsets?
Reveal
Offsets of an empty group expire after
offsets.retention.minutes(7 days). On restart the group behaves like a new one and followsauto.offset.reset.
8.1Committed offset, position, and lag
- Committed offset: stored in Kafka for the group. It is where any member restarts reading this partition, after a crash or a rebalance.
- Position: the next offset this consumer instance will fetch. It runs ahead of the committed offset while records are being processed.
- High watermark: the end of the records that are replicated to all in-sync replicas. Consumers cannot read past it.
- Log end offset: the next offset the leader will assign. Records between the high watermark and the log end are not yet fully replicated.
- Lag = high watermark minus committed offset, per partition. Watch lag as a trend and in time ("the oldest unread record is 4 minutes old"), not only as a count.
A committed offset always names the next record to read: last processed offset plus one.
8.2Automatic commits
enable.auto.commit=trueis the client default. Everyauto.commit.interval.ms(5 seconds), the nextpoll()commits the offsets of records returned by earlier polls.close()commits too.- Safe enough when you process each batch fully before calling
poll()again: a crash replays at most a few seconds of records (at-least-once). - Unsafe when processing is asynchronous: if you hand records to a thread pool and keep polling, auto-commit may record them as done before the workers finish, and a crash loses them.
- Spring for Apache Kafka turns auto-commit off by default and commits for you after your listener returns (Module 18).
8.3Committing yourself
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(500));
for (ConsumerRecord<String, String> r : records) {
process(r);
}
consumer.commitAsync((offsets, e) -> { // fast path, no retries
if (e != null) log.warn("commit failed, a later commit will cover it", e);
});
}
// on shutdown or partition revocation: one blocking commit that retries
consumer.commitSync();| Method | Blocks | Retries | Use |
|---|---|---|---|
commitSync() |
Yes | Yes, until success or a fatal error | Shutdown, rebalance revocation, strict pipelines |
commitAsync(callback) |
No | No (a late retry could overwrite a newer commit) | The normal loop |
commitSync(Map<TopicPartition, OffsetAndMetadata>) |
Yes | Yes | Commit exact offsets, for example after each record or each sub-batch |
- To commit a specific record, commit
new OffsetAndMetadata(record.offset() + 1)for its partition. CommitFailedExceptionmeans the group rebalanced and this member no longer owns the partition; its uncommitted records will be reprocessed by the new owner. It usually signals processing that is too slow formax.poll.interval.ms(Module 9).- Commit frequency trades duplicates against overhead: committing after every record minimizes replays but adds a write per record.
8.4Delivery semantics
Your consumer sends an SMS for each record. Should it commit before or after sending? What does each choice cost after a crash?
Reveal the answer
Committing before sending (at-most-once) can skip SMS messages that were committed but never sent. Committing after (at-least-once) can send a few SMS twice. For alerts, at-least-once plus deduplication by event ID is the usual choice: a rare duplicate is better than a missed fraud alert.
| Semantics | How | Failure cost | Use for |
|---|---|---|---|
| At-most-once | Commit, then process | Records can be lost | Metrics where loss is fine |
| At-least-once | Process, then commit | Records can repeat | The default for almost everything |
| Exactly-once (effect) | At-least-once delivery plus idempotent processing, or transactions | More design work | Money, inventory, counts that must be exact |
Kafka transactions give exactly-once for read-process-write pipelines that stay inside Kafka (Module 14). When the effect is in another system, make the effect idempotent.
8.5Making at-least-once safe
- Idempotent processing: record each event's ID in a table with a unique constraint, in the same database transaction as the effect; a redelivery hits the constraint and is skipped (Module 10).
- Natural idempotency: upserts by key (
INSERT ... ON CONFLICT UPDATE), setting absolute values ("balance is 500") instead of deltas ("add 100"), or version checks that ignore older versions. - Store offsets with the results: write the effect and the next offset in one database transaction, and on startup seek to the stored offset. Use
assign()or seek inside a rebalance listener.
8.6Replaying and resetting offsets
- Command line (the group must have no active members):
# preview, then apply: --dry-run is the default, --execute applies
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group billing --topic orders \
--reset-offsets --to-earliest --dry-run
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group billing --topic orders \
--reset-offsets --to-datetime 2026-10-17T09:00:00.000 --execute| Option | Moves the committed offset to |
|---|---|
--to-earliest, --to-latest |
The start or end of each partition |
--to-datetime 'YYYY-MM-DDThh:mm:ss.sss' |
The first record at or after that time |
--by-duration PT1H |
The first record from one hour ago |
--shift-by -100 |
100 records back (positive moves forward) |
--to-offset N |
Exactly N |
- In code:
seek(partition, offset),seekToBeginning,seekToEnd, andoffsetsForTimesto find the offset for a timestamp. - Expiry: committed offsets of an empty group are deleted after
offsets.retention.minutes(7 days by default). A consumer that is off for longer restarts according toauto.offset.reset.
Before a replay, check that consumers are idempotent and that downstream systems can take the extra load. Replaying a day of payments into a non-idempotent ledger consumer double-books every one of them.
8.7Lab: lag and offset reset
Continue from the Module 7 lab. Stop every billing consumer, then produce more records:
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic orders \
--num-records 1000 --record-size 100 --throughput -1
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billingPlan and apply a rewind of 10 records per partition, then a reset to the beginning:
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group billing --topic orders \
--reset-offsets --shift-by -10 --dry-run
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group billing --topic orders \
--reset-offsets --shift-by -10 --execute
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group billing --topic orders \
--reset-offsets --to-earliest --execute
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billingNow try a reset while one consumer of the group is running.
Check what you saw
- After producing, the
LAGvalues add up to 1,000 across the three partitions (keyless records are spread batch by batch, so not exactly evenly), and the output notes the group has no active members. --dry-runprints the plannedNEW-OFFSETwithout changing anything;--executeapplies it.- After
--to-earliest,CURRENT-OFFSETis 0 andLAGequals the whole partition, so a restarted consumer re-reads everything. - With a member running, the reset is refused because the group is not inactive. Offsets are owned by the live members.
8.8Common mistakes
Common mistakes
- Committing
record.offset()instead ofrecord.offset() + 1, which replays one record per partition after every restart. - Auto-commit with asynchronous processing, which can lose records on a crash.
- Retrying
commitAsyncin its callback, which can overwrite a newer committed offset with an older one. - Ignoring
CommitFailedExceptioninstead of fixing slow processing. - Replaying into non-idempotent consumers.
- Leaving a group stopped longer than offset retention and silently skipping to
latest.
8.9Self-check
- Define committed offset, position, high watermark, and lag.
Reveal
Committed offset: the group's stored restart point. Position: the next offset this instance will fetch. High watermark: the end of fully replicated, readable records. Lag: high watermark minus committed offset.
- Why does
commitAsyncnot retry?Reveal
Commits can finish out of order. Retrying an older commit after a newer one succeeded would move the committed offset backwards and cause reprocessing.
- How do you get at-least-once delivery with the plain consumer?
Reveal
Process each record (or batch) fully, then commit; never commit offsets for records not yet processed.
- How do you get exactly-once effects when the consumer writes to PostgreSQL?
Reveal
Make the write idempotent (unique event ID or upsert in the same transaction as the effect), or store the next offset in the same database transaction as the effect and seek to it on startup.
- What must be true before
kafka-consumer-groups.sh --reset-offsets --executeworks?Reveal
The group must have no active members. Stop the consumers first.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 9: Rebalancing, the new group protocol, and share groups
A rebalance moves partitions between the members of a group. Done badly, it stops processing and causes duplicates; done well, nobody notices. This module covers what triggers rebalances, the two liveness timers, the classic protocol with eager and cooperative assignment, static membership, rebalance listeners, the KIP-848 protocol that is GA since Kafka 4.0, and share groups for queue-style consumption.
- Name three events that trigger a rebalance.
Reveal
A consumer joins, a consumer leaves or is declared dead (missed heartbeats or exceeded
max.poll.interval.ms), or the subscription changes (new partitions, or a new topic matching a pattern). - Processing one batch takes 8 minutes with default settings. What happens?
Reveal
The gap between polls exceeds
max.poll.interval.ms(5 minutes), so the consumer leaves the group, its partitions go to others, and its later commit fails withCommitFailedException. - In an eager rebalance, which partitions stop being processed?
Reveal
All of them. Every member revokes every partition before the new assignment.
- What does
group.instance.iddo?Reveal
It makes the consumer a static member: a restart that comes back within the session timeout keeps its partitions without a rebalance.
- How do share groups differ from consumer groups?
Reveal
Members share partitions and acknowledge records one by one, so a group can have more consumers than partitions, but there is no per-key ordering.
9.1What triggers a rebalance
- Membership changes: a consumer starts, shuts down cleanly (it sends a leave request), or crashes.
- Liveness failures: missed heartbeats for
session.timeout.ms, or nopoll()call withinmax.poll.interval.ms. - Metadata changes: partitions added to a subscribed topic, or a new topic that matches a subscribed pattern.
Every rebalance has a cost: partitions pause while they move, and records processed but not yet committed are processed again by the new owner.
9.2Two liveness timers
A consumer's heartbeats arrive every 3 seconds, but it has been stuck in one database call for 7 minutes. Is it still in the group?
Reveal the answer
No. Heartbeats come from a background thread, so they prove only that the process is alive. The separate max.poll.interval.ms check (5 minutes) notices that the loop has not called poll(), and the consumer leaves the group so its partitions can move to a healthy member.
poll() within max.poll.interval.ms or the consumer leaves the group because it looks stuck.| Setting | Default | Guards against | Tune when |
|---|---|---|---|
heartbeat.interval.ms |
3 s | n/a (how often to heartbeat) | Keep at about a third of the session timeout |
session.timeout.ms |
45 s | Dead process or lost network | Lower for faster failover, higher for flaky networks |
max.poll.interval.ms |
5 min | Stuck or slow processing | Raise if a batch legitimately takes long, or lower max.poll.records |
9.3The classic protocol
- The group coordinator is the broker that leads the group's partition of
__consumer_offsets. - Each rebalance bumps the group's generation. Commits from an older generation are rejected, which is how zombie members are kept from committing.
- The group leader (one of the consumers) runs the assignor locally, which is why all members must agree on the assignor.
| Assignor | Spreads partitions | Rebalance style |
|---|---|---|
RangeAssignor |
Per topic, contiguous ranges | Eager |
RoundRobinAssignor |
Across all subscribed partitions in turn | Eager |
StickyAssignor |
Balanced, keeps existing ownership | Eager |
CooperativeStickyAssignor |
Balanced, keeps existing ownership | Cooperative |
The default list is [RangeAssignor, CooperativeStickyAssignor], so groups use Range. To move to cooperative rebalancing, do one rolling restart with only CooperativeStickyAssignor in the list.
9.4Eager and cooperative rebalancing
- Eager: every member revokes everything, then receives a new assignment. Processing stops group-wide for the whole rebalance.
- Cooperative (incremental): members keep the partitions that are not moving. A first round revokes only what must move; a second round assigns it. Most partitions never pause.
- Cooperative rebalancing makes rolling deploys of large groups dramatically smoother.
9.5Static membership
- Set a unique, stable
group.instance.idper instance (for example the Kubernetes pod name in a StatefulSet). - A static member that restarts and returns within
session.timeout.msgets its partitions back without a rebalance. Raise the session timeout to cover a normal restart, for example 60 to 120 seconds. - Trade-off: if the instance truly dies, its partitions wait for the session timeout before moving.
9.6Rebalance listeners
consumer.subscribe(List.of("payments"), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
consumer.commitSync(currentOffsets(partitions)); // last chance to record progress
flushLocalState(partitions);
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
loadLocalState(partitions); // or seek to offsets stored elsewhere
}
@Override
public void onPartitionsLost(Collection<TopicPartition> partitions) {
dropLocalState(partitions); // already owned by someone else: do not commit
}
});onPartitionsRevokedruns before ownership moves: commit offsets and flush buffered work.onPartitionsAssignedruns after: load state, orseek()to offsets you store yourself.onPartitionsLostruns when ownership was lost without a chance to hand over (for example after a session timeout); committing then would fail.
9.7The new consumer group protocol (KIP-848)
- Generally available since Kafka 4.0 and enabled on brokers by default. Clients opt in with
group.protocol=consumer; the client default is stillclassic, and the project plans to makeconsumerthe default in Kafka 5.0. - The broker computes assignments with server-side assignors (
uniform, the default, andrange). There is no group leader and no JoinGroup or SyncGroup barrier. - Fully incremental: each member is told only what to give up and what to take; everyone else keeps working.
- Configuration moves to the broker: with the new protocol, the client settings
session.timeout.ms,heartbeat.interval.ms, andpartition.assignment.strategyare not used. The broker'sgroup.consumer.session.timeout.ms(45 s) andgroup.consumer.heartbeat.interval.ms(5 s) apply instead. - Migration: roll consumers to
group.protocol=consumerone at a time; a group converts while online, as long as it uses an assignor without custom metadata. Client-side custom assignors are not supported.
Kafka Streams has its own broker-side protocol (KIP-1071), production-ready for its core features since Kafka 4.2.
9.8Share groups: queues for Kafka
- Production-ready since Kafka 4.2 and enabled by default on new 4.2+ clusters. Use
KafkaShareConsumer(orkafka-console-share-consumer.sh) with a group ID. - Records, not partitions, are handed out. Each fetched record is acquired with a lock (
share.record.lock.duration.ms, 30 s by default). The consumer acknowledges it asACCEPT,RELEASE(try again),REJECT(unprocessable), orRENEW(needs more time). - Delivery attempts are counted. After
share.delivery.count.limitattempts (5 by default) a record is archived instead of redelivered. - Acknowledgement modes:
share.acknowledgement.mode=implicit(default) accepts everything from a poll on the next poll or commit;explicitrequiresacknowledge()per record. - Use them for job queues: independent tasks where throughput matters more than order, and where you want more workers than partitions. Keep consumer groups for ordered event streams.
9.9Avoiding rebalance storms
- Keep each poll loop iteration short: lower
max.poll.records, or move slow work out of the loop safely. - Use cooperative rebalancing (CooperativeSticky or the new protocol).
- Use static membership for rolling restarts.
- Shut down cleanly: call
wakeup(), commit,close()(Module 10). - Watch for long GC pauses and CPU throttling in containers, which look like dead consumers.
- Monitor the rebalance rate (
rebalance-rate-per-houron the consumer,consumer-group-rebalance-rateon the coordinator).
9.10Lab: watch a rebalance, then try a share group
On the single broker, start two consumers (in two terminals) in one group on the 3-partition orders topic from Module 7, both using the new protocol:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic orders --group rebal \
--command-property group.protocol=consumerDescribe the members, then stop one consumer and describe again:
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group rebal --members --verbose
./kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group rebal --stateNow a share group. Run the first command in four separate terminals (four share consumers on a 3-partition topic), then produce records and describe the group:
./kafka-console-share-consumer.sh --bootstrap-server localhost:9092 --topic orders --group jobs
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic orders \
--num-records 400 --record-size 50 --throughput 50
./kafka-share-groups.sh --bootstrap-server localhost:9092 --describe --group jobs --membersCheck what you saw
--members --verboselists each member's assigned partitions: one member has two, the other one.--stateshows the assignment strategy (uniform, the broker-side default), the group state, and the member count.- After you stop one consumer, its partitions move to the other within seconds.
- Several share consumers, often all four, print records even though there are only three partitions: records, not partitions, are handed out. Records from one partition can appear in different terminals, so their order is not preserved.
kafka-share-groups.sh --describeshows members and their partitions; several members can share the same partition.
9.11Common mistakes
Common mistakes
- Slow processing inside the loop with default
max.poll.records, causing the member to be kicked out again and again. - Not committing in
onPartitionsRevoked, so the new owner reprocesses everything since the last commit. - Committing in
onPartitionsLost, which fails because the partitions already belong to someone else. - Eager assignors in large groups, where every deploy stops all processing.
- Static membership with a short session timeout, which gives no benefit, or with a very long one, which delays recovery after real failures.
- Using share groups where per-key order matters.
9.12Self-check
- Why are there two liveness timers?
Reveal
Heartbeats from a background thread detect a dead process quickly;
max.poll.interval.msdetects a live process whose processing loop is stuck. Each catches a failure the other cannot. - In the classic protocol, who computes the assignment?
Reveal
One consumer, elected group leader by the coordinator, runs the assignor and sends the result in its SyncGroup request; the coordinator hands each member its share.
- What makes cooperative rebalancing cheaper than eager?
Reveal
Members keep the partitions that are not moving and keep processing them; only partitions that change owner are revoked, over two short rounds.
- What changes for a client that sets
group.protocol=consumer?Reveal
Assignment is computed by the broker, rebalances are incremental with no group-wide barrier, and heartbeat, session timeout, and assignor settings come from the broker instead of the client.
- When would you choose a share group over a consumer group?
Reveal
For independent tasks where you need more parallel workers than partitions, per-record acknowledgement, and delivery counting, and where per-key ordering does not matter.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 10: Consumer failure handling, retries, and dead letters
Real consumers meet malformed records, flaky dependencies, slow databases, and redeliveries. This module turns those into design choices: blocking versus non-blocking retries, dead letter topics that someone actually watches, keeping per-key order while retrying, idempotent consumers, backpressure, and clean shutdown.
- One malformed record makes your consumer throw every time. What happens to the rest of its partition?
Reveal
It is stuck behind that record (head-of-line blocking): the consumer never advances, and lag grows on that partition.
- Should a validation error and a database timeout be retried the same way?
Reveal
No. A timeout is transient and worth retrying with backoff. A validation error is permanent; retrying cannot fix it, so send it straight to a dead letter topic.
- Retry topics keep the main consumer flowing. What do they cost?
Reveal
Ordering. The failed record is processed later, after records that came behind it.
- Why must consumers be idempotent even with careful commits?
Reveal
Delivery is at-least-once: a crash or rebalance between processing and committing redelivers records.
- How do you stop a consumer cleanly from another thread?
Reveal
Call
consumer.wakeup(). The blockedpoll()throwsWakeupException; catch it, commit, andclose().
10.1Kinds of failure
| Failure | Example | Retry? | Typical handling |
|---|---|---|---|
| Transient | Timeout, HTTP 503, deadlock, leader change | Yes, with backoff | Retry in place or via retry topics |
| Permanent (poison) | Invalid amount, unknown merchant, schema mismatch | No | Dead letter topic, alert, fix, replay |
| Deserialization | Bytes that are not valid JSON or Avro | No | Capture raw bytes to a dead letter topic, skip |
| Consumer crash | Out of memory, pod killed | Automatic | Rebalance; records since the last commit are redelivered |
| Slow downstream | Database at 100% CPU | Slow down | Backpressure: pause, smaller batches, circuit breaker |
10.2Poison messages
- A poison message blocks its partition if the consumer keeps retrying it. Other partitions are fine, which is a useful diagnostic: lag on one partition only.
- Deserialization errors surface inside
poll(). The Java client throwsRecordDeserializationException, which carries the partition and offset: log the raw record, send it to a dead letter topic, thenseek()to offset + 1. Alternatively consumebyte[]and deserialize in your own code where you can catch errors per record. Spring offersErrorHandlingDeserializerfor this (Module 18). - Bound every retry loop. No record deserves infinite attempts.
10.3Retry strategies
A downstream fraud API is down for 10 minutes. Compare (a) retrying each record in place with backoff, and (b) moving failures to a retry topic with a 1-minute delay. What happens to throughput, ordering, and the API in each?
Reveal the answer
(a) The partition stalls for 10 minutes but stays in order; backoff keeps load on the API low. Lag builds and then drains. (b) The main consumer keeps flowing, but every record fails into the retry topic, then fails again; nothing succeeds until the API returns, ordering is lost, and the retry topics fill up. When the dependency itself is down, pausing (a, or a circuit breaker) beats retry topics; retry topics shine when only some records fail.
| Blocking retry in place | Non-blocking retry topics | |
|---|---|---|
| How | Seek back to the failed offset, back off, try again; pause the partition while waiting | Publish the failed record to topic-retry-1m, topic-retry-10m; consumers there wait until due |
| Order | Preserved | Lost for the retried record |
| Partition progress | Stops during retries | Continues |
| Best for | Ordered streams; a dependency that is fully down | Occasional failures of individual records |
| Watch out | max.poll.interval.ms: back off with pause(), not sleep() |
Retry storms when everything fails; topic sprawl |
- Exponential backoff with jitter for in-place retries, and a cap on attempts.
- A circuit breaker around the dependency: after repeated failures, pause the partitions instead of failing record after record.
- Retry topics delay by waiting: the retry consumer reads a record, and if it is not yet due, pauses the partition until it is. Kafka has no per-message delay (Case study 8).
10.4Dead letter topics
A dead letter topic (DLT) holds records that could not be processed after all attempts. It is a work queue for people, not a trash can.
- Keep the original record: same key and value bytes, so it can be replayed as is.
- Add headers that explain it: original topic, partition, offset, timestamp, exception class and message, attempt count, consumer group, and application version.
- Alert on any DLT traffic, and show the reason in a dashboard.
- Build a replay tool: after a fix, republish selected DLT records to the original topic (or a retry topic).
- Give the DLT long retention, longer than your investigation time.
try {
process(record);
} catch (NonRetryableException e) {
ProducerRecord<byte[], byte[]> dead =
new ProducerRecord<>("payments-dlt", record.key(), record.value());
record.headers().forEach(h -> dead.headers().add(h));
dead.headers()
.add("dlt.original.topic", record.topic().getBytes(UTF_8))
.add("dlt.original.partition", Integer.toString(record.partition()).getBytes(UTF_8))
.add("dlt.original.offset", Long.toString(record.offset()).getBytes(UTF_8))
.add("dlt.exception", e.toString().getBytes(UTF_8));
dltProducer.send(dead).get(); // wait: it must be in the DLT before the offset is committed
}10.5Keeping per-key order while retrying
When order per key matters (account balance events), a retried record must not be overtaken by later records of the same key:
- Block the partition: retry in place. Simple and correct; one bad key stalls every key in that partition.
- Park the key: keep a set of "blocked keys". When a record of a blocked key arrives, send it to a parking topic in order instead of processing it. When the head record finally succeeds (or is resolved by a person), replay that key's parked records in order. Other keys keep flowing. It is more complex but isolates one bad entity.
- Decide per use case: notifications rarely need strict order; ledgers always do.
10.6Idempotent consumers
CREATE TABLE processed_events (
consumer_group text NOT NULL,
event_id text NOT NULL,
processed_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (consumer_group, event_id)
);
BEGIN;
INSERT INTO processed_events (consumer_group, event_id)
VALUES ('ledger', 'e-77')
ON CONFLICT DO NOTHING; -- 0 rows inserted: duplicate, skip the effect
UPDATE accounts SET balance = balance - 1250 WHERE id = 42; -- only if 1 row was inserted
COMMIT;- The event ID must come from the producer (a UUID or a business key), not from Kafka offsets, which change if records are republished.
- Keep dedup rows longer than any redelivery or replay window, then prune them.
- Other options: upsert by business key, absolute values instead of increments, or version numbers that ignore stale updates.
10.7Backpressure and slow consumers
pause()andresume(): stop fetching specific partitions while still callingpoll(), so the consumer stays in the group. Use it when a downstream is unhealthy or a worker queue is full.- Smaller
max.poll.recordskeeps each loop iteration withinmax.poll.interval.ms. - Batch downstream writes: one database round trip for 500 records instead of 500.
- Scale out up to the partition count. Autoscalers such as KEDA can scale consumers on lag.
- Shed or degrade non-critical work when lag threatens retention (Module 20 has the lag drain math).
10.8Clean shutdown
final Thread mainThread = Thread.currentThread();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
consumer.wakeup(); // makes the blocked poll() throw WakeupException
try { mainThread.join(); } catch (InterruptedException ignored) { }
}));
try {
consumer.subscribe(List.of("payments"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(1));
for (ConsumerRecord<String, String> r : records) process(r);
consumer.commitAsync();
}
} catch (WakeupException e) {
// expected during shutdown
} finally {
try {
consumer.commitSync(); // final, retried commit
} finally {
consumer.close(); // leaves the group promptly: fast rebalance
}
}10.9Lab: build a dead letter flow
In your Java project (see Lab setup):
- Create topics
paymentsandpayments-dlton the single broker. - Write a consumer for
paymentsthat parses each value as an integer amount. Treat a non-number as a poison record; treat an amount of exactly503as a transient error that succeeds on the third attempt. - Retry transient errors in place up to 5 times with backoff, using
pause()andseek()rather thanThread.sleep()for long waits. - Send poison records to
payments-dltwith the headers from this module, then continue. - Commit only after a record succeeded or reached the DLT.
- Produce
100,abc,503,250with the console producer and watch the logs. Then readpayments-dltwith--formatter-property print.headers=true.
Check what you saw
100and250succeed at once;503succeeds on attempt 3 while the partition waits, and250is processed after it (order kept).abcappears inpayments-dltwith the original topic, partition, offset, and exception in its headers.- If you kill the consumer between processing and committing, restarting it reprocesses that record: run the same input twice and check your output for duplicates. That is the reason for the next step: make
processidempotent by event ID.
10.10Common mistakes
Common mistakes
- Infinite retries that block a partition forever.
- Retrying permanent errors such as validation failures.
Thread.sleep()inside the poll loop for long backoffs, which exceedsmax.poll.interval.msand triggers rebalances.- A DLT that nobody alerts on or replays from.
- Retry topics for an ordered stream without a plan for order.
- Deduplicating by Kafka offset instead of a producer-assigned event ID.
- Killing consumers without
close(), which delays the rebalance until the session times out.
10.11Self-check
- When are retry topics a bad idea?
Reveal
When per-key order matters, or when a dependency is completely down (every record would cycle through the retry topics). Use in-place retries, pausing, or a circuit breaker instead.
- What should a DLT record contain?
Reveal
The original key, value, and headers, plus metadata: original topic, partition, offset, timestamp, exception details, attempt count, and the consumer that failed.
- How do you skip a record that cannot be deserialized?
Reveal
Catch
RecordDeserializationException, record the raw bytes (for example to a DLT), andseek()to its offset + 1. Or consume bytes and deserialize yourself. - How does a processed-events table make a consumer idempotent?
Reveal
Inserting the event ID with a unique constraint in the same transaction as the effect means a duplicate insert fails or inserts nothing, so the effect runs once.
- Why use
pause()rather than stopping calls topoll()?Reveal
poll()keeps the consumer alive in the group and lets other partitions continue; not polling would exceedmax.poll.interval.msand trigger a rebalance.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 11: Storage: the log on disk
Why can a Kafka broker serve hundreds of megabytes per second from ordinary disks? This module opens a partition directory: segments, sparse indexes, record batches, the page cache, zero-copy transfers, how retention deletes data, why Kafka does not fsync every write, and how tiered storage moves history to object storage.
- Is one partition stored as one big file?
Reveal
No. It is a directory of segment files. New records go to the active segment; older segments are sealed.
- How does Kafka find offset 52,400 without scanning gigabytes?
Reveal
It picks the segment by base offset from its file name, looks up the nearest earlier entry in that segment's sparse index, and scans forward a few kilobytes.
- Does Kafka keep its own cache of recent records in the JVM heap?
Reveal
No. It relies on the operating system's page cache, so recently written data is usually served from memory.
- Does Kafka fsync each write to disk before acknowledging it?
Reveal
Not by default. Durability comes from replication to several brokers; flushing to disk is left to the operating system.
- Retention is 7 days. Can a record older than 7 days still be read?
Reveal
Yes, for a while. Retention deletes whole segments, and a segment goes only when its newest record is older than the limit.
11.1A partition is a directory of segments
- Each partition replica is a directory named
<topic>-<partition>under one of the broker'slog.dirs. - A segment is a
.logfile of record batches plus a.index(offset to byte position) and a.timeindex(timestamp to offset). The file names are the segment's base offset, the first offset it contains. - The active segment receives appends. It rolls (is sealed and a new one starts) when it reaches
segment.bytes(1 GiB by default), when it is older thansegment.ms(7 days), or when its index fills up. - Other files you may see:
.txnindex(aborted transactions, Module 14),.snapshot(producer state for idempotence),leader-epoch-checkpoint, andpartition.metadata(the topic ID).
11.2Finding an offset
- Segment lookup: brokers keep segments sorted by base offset, so finding the right one is a binary search.
- Sparse index: one entry per
log.index.interval.bytes(4 KB) of log data, mapping a relative offset to a byte position. Index files are memory-mapped and small. - Short scan: from that position, read forward to the exact offset. Reads are sequential and usually hit the page cache.
- The time index works the same way for timestamps. It answers "first offset at or after 09:00", which is how offset resets by time and
offsetsForTimeswork.
11.3Record batches
- Producers send, brokers store, and consumers receive record batches, not single records. A batch header holds the base offset, the leader epoch, timestamps, a CRC, compression and transaction flags, and the producer ID, epoch, and base sequence number used for idempotence.
- Records inside a batch store small deltas (offset delta, timestamp delta) with variable-length encoding, so many small records cost little overhead.
- With the topic's default
compression.type=producer, the broker keeps batches compressed exactly as the producer sent them and serves them as is; consumers decompress.
11.4Why Kafka is fast
List four design choices that let one broker handle very high throughput on ordinary hardware.
Reveal the answer
Sequential appends instead of random writes; the operating system's page cache instead of an application cache; zero-copy transfer from page cache to network for consumers; batching and compression end to end; and spreading partitions over many disks and brokers.
- Sequential I/O: appends and tail reads are sequential, which disks (even spinning ones) do very fast.
- The page cache: Kafka writes into the operating system's page cache and reads from it. Consumers that keep up read data that is still in memory, so many caught-up clusters show almost no disk reads. Give brokers a modest JVM heap (a few GB) and leave the rest of RAM to the page cache.
- Zero-copy: the broker sends file data straight from the page cache to the socket.
- Batching and compression shrink request counts and bytes on the wire and on disk.
- No per-message bookkeeping on the broker: consumers track their own offsets, so a read does not change broker state.
TLS encryption happens in user space, so brokers cannot use sendfile for TLS connections. Expect more broker CPU and somewhat lower throughput with TLS; plan capacity with it on, since production clusters should encrypt.
11.5Retention and deletion
- A background task runs every
log.retention.check.interval.ms(5 minutes). Withcleanup.policy=delete, it deletes a sealed segment when its newest record is older thanretention.ms, or deletes the oldest segments while the partition is larger thanretention.bytes. - Deleted segments are renamed with a
.deletedsuffix and removed afterlog.segment.delete.delay.ms(1 minute). - The log start offset moves to the first remaining offset. A consumer asking for an older offset gets "offset out of range" and resets per
auto.offset.reset. kafka-delete-records.sh(thedeleteRecordsAPI) moves the log start offset forward on demand, for example to purge bad data.- To delete data sooner, lower
segment.msas well asretention.ms, because only sealed segments can be deleted.
11.6Durability without fsync
- By default Kafka does not force an fsync for each write:
flush.messagesandflush.msare effectively unlimited, so the operating system decides when dirty pages reach disk. - Durability comes from replication: an
acks=allwrite sits in the page cache of every in-sync replica, on different machines. Losing all of them at once, before any flushes, is far less likely than losing one disk. - After a crash, a broker recovers its unflushed segments (rebuilding indexes, truncating partial batches) and then catches up from the current leader.
- You can force frequent flushes per topic, but it costs throughput; replication with
acks=allandmin.insync.replicas=2is the standard answer.
11.7Tiered storage
- Generally available since Kafka 3.9. Brokers copy completed segments to remote object storage and keep only recent data locally (
local.retention.msorlocal.retention.bytes);retention.msstill sets total retention. - Why: long retention (months or years) without huge broker disks; faster broker replacement and reassignment because less data lives locally.
- Setup: enable
remote.log.storage.system.enable=trueon brokers, plug in aRemoteStorageManagerimplementation for your store (Kafka does not ship one for S3), then setremote.storage.enable=trueper topic. - Limits: not supported for compacted topics; reads of old data are slower than local tail reads.
11.8Lab: look inside a partition
On the single broker, create a topic with tiny 1 MB segments (the minimum allowed) and fill it:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic seglab --partitions 1 \
--replication-factor 1 --config segment.bytes=1048576
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic seglab \
--num-records 5000 --record-size 1000 --throughput -1
ls -l /tmp/kraft-combined-logs/seglab-0/Dump the first segment and its index:
./kafka-dump-log.sh --files /tmp/kraft-combined-logs/seglab-0/00000000000000000000.log | head -8
./kafka-dump-log.sh --files /tmp/kraft-combined-logs/seglab-0/00000000000000000000.index | head -8Now make retention very short, wait about 6 minutes, and check the files and the earliest offset:
./kafka-configs.sh --bootstrap-server localhost:9092 --alter --entity-type topics \
--entity-name seglab --add-config retention.ms=60000
ls -l /tmp/kraft-combined-logs/seglab-0/
./kafka-get-offsets.sh --bootstrap-server localhost:9092 --topic seglab --time earliestCheck what you saw
- About five
.logfiles appear, each near 1 MB, named by their base offsets, each with a.indexand.timeindex. - The log dump prints batches with
baseOffset,lastOffset,count,producerId,baseSequence, andcompresscodec. The index dump shows a sparse list ofoffsetandpositionpairs, not one per record. - After the retention check runs (every 5 minutes) all sealed segments are deleted; files briefly carry a
.deletedsuffix. Only the active segment remains, and the earliest offset jumps from 0 to that segment's base offset.
11.9Common mistakes
Common mistakes
- Giving the broker JVM most of the machine's RAM, starving the page cache that Kafka actually relies on.
- Expecting retention to delete data exactly on time: whole segments go, and only sealed ones.
- Forcing fsync per message for "safety" instead of using replication and
acks=all. - Running consumers far behind, so their reads miss the page cache and hit disk, slowing everyone.
- Turning on tiered storage for compacted topics, which it does not support.
- Forgetting TLS costs when sizing broker CPU.
11.10Self-check
- What files make up a segment, and what does each do?
Reveal
The
.logholds record batches; the.indexmaps relative offsets to byte positions; the.timeindexmaps timestamps to offsets. The name is the base offset. - Why is the offset index sparse?
Reveal
Indexing every record would make the index large. One entry per 4 KB keeps it small and memory-mapped; a short sequential scan finds the exact record.
- What is zero-copy, and when does Kafka lose it?
Reveal
Using
sendfileto move data from the page cache directly to the network socket without copying through the application. It is not possible with TLS, which must encrypt in user space. - When exactly does time-based retention delete a segment?
Reveal
When the segment is sealed and its newest record's timestamp is older than
retention.ms, at the next retention check. - Where does Kafka's durability come from if it does not fsync each write?
Reveal
From replication: an acknowledged
acks=allrecord is in memory or on disk on every in-sync replica, on separate machines.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 12: Replication and fault tolerance
Replication is how Kafka survives broker failures without losing acknowledged data. This module goes inside it: how followers fetch, what keeps a replica in the ISR, the log end offset and high watermark, leader election and epochs, truncation, the unclean election trade-off, eligible leader replicas, and rack-aware placement.
- Do followers receive data pushed by the leader?
Reveal
No. Followers send fetch requests to the leader, like consumers, and append what they receive.
- What gets a follower removed from the ISR?
Reveal
Not catching up to the leader's log end within
replica.lag.time.max.ms(30 s), or losing its session with the controller (the broker is fenced). - Can a consumer read a record the moment the leader writes it?
Reveal
No. Consumers read only below the high watermark, which advances when every in-sync replica has the record.
- What does unclean leader election trade?
Reveal
Availability for durability. It lets an out-of-sync replica become leader, so the partition comes back sooner but acknowledged records can be lost.
- An old leader comes back after a failover with records the new leader never got. What happens to them?
Reveal
The old leader truncates them, using leader epochs to find where its log diverged, and then follows the new leader.
12.1Leaders, followers, and fetching
- Every partition has one leader and zero or more followers on other brokers. Producers write to the leader; consumers read from it by default.
- Followers run replica fetcher threads that send fetch requests to the leader and append the returned batches, so their logs are identical copies with the same offsets.
- Leaders are spread across brokers so that write load is balanced. The first replica in a partition's replica list is its preferred leader.
12.2The in-sync replica set
- A replica is in sync when its broker has an active session with the controller and it has caught up to the leader's log end within the last
replica.lag.time.max.ms(30 s). - The leader shrinks the ISR when a follower falls behind and expands it when the follower catches up; the controller records each change in the metadata log.
- The guarantee: a committed record will not be lost as long as at least one in-sync replica survives.
12.3Log end offset and high watermark
acks=all producer is answered when the HW passes its records.- Log end offset (LEO): on each replica, the offset the next record will get.
- High watermark (HW): on the leader, the highest offset that every in-sync replica has; records below it are committed. Followers learn the HW from fetch responses.
- Consumers see only committed records, so a consumer never reads a record that a failover could erase.
- An
acks=allproduce request completes once the HW passes its records. Since Kafka 3.7 the HW also stops advancing while the ISR is smaller thanmin.insync.replicas.
12.4acks and failures together
| Scenario (RF 3, min ISR 2) | acks=1 |
acks=all |
|---|---|---|
| Leader dies right after acknowledging, before followers fetch | The acknowledged record is lost | Cannot happen: acknowledgment waited for the ISR |
| One follower down | Writes fine | Writes fine (ISR of 2 meets the minimum) |
| Two replicas down | Writes accepted, not visible to consumers | Writes rejected with NotEnoughReplicas |
| All in-sync replicas lost at once | Data since the last replicated point is lost | Partition offline until an in-sync replica returns |
12.5Leader election and epochs
- The controller notices a dead broker when its heartbeats stop for
broker.session.timeout.ms(9 s) and fences it. A clean shutdown is faster: the broker asks the controller to move its leaderships first (controlled shutdown). - The controller picks a new leader from the ISR and increases the partition's leader epoch. Every replica and client uses the epoch to reject stale leaders and stale requests.
- Clients get
NOT_LEADER_OR_FOLLOWERor connection errors, refresh metadata, and retry against the new leader automatically. - Truncation: a returning replica asks the leader where the log of its last epoch ended, then truncates anything beyond it before fetching. This is how records that were never committed disappear, and why
acks=1writes can vanish.
12.6Unclean leader election
- If every in-sync replica is gone, the partition has no safe leader.
unclean.leader.election.enable=false(default): stay offline until an in-sync replica returns. No acknowledged data is lost; availability suffers.true: elect any live replica now. The partition is available sooner, but records only the lost replicas had are gone, and later writes reuse their offsets.- Choose
falsefor anything financial or auditable. Some teams enable it on low-value topics such as metrics.
Go deeperEligible leader replicas (Kafka 4.x)
Kafka 4.0 introduced eligible leader replicas (ELR, KIP-966 part 1), enabled by default on new clusters since 4.1. Because the high watermark cannot advance while the ISR is below min.insync.replicas, a replica that drops out of the ISR in that state is still guaranteed to hold every committed record. The controller tracks such replicas as the ELR and may elect one of them if the ISR becomes empty, before falling back to the last known leader. The practical effect: fewer cases where the only choices are "stay offline" or "unclean election and lose data". With ELR enabled, min.insync.replicas must be set at the cluster or topic level, not per broker.
12.7Preferred leaders and balance
- After a broker restarts, it rejoins as a follower and leads nothing, so load is skewed toward the others.
auto.leader.rebalance.enable=true(default) moves leadership back to preferred leaders periodically (checked every 5 minutes by default). To do it now, runkafka-leader-election.sh --election-type preferred --all-topic-partitions.- Spread preferred leaders evenly when you create or reassign topics; tools such as Cruise Control keep replicas and leaders balanced automatically.
12.8Racks, zones, and follower fetching
- Set
broker.rackto each broker's availability zone. Kafka then places each partition's replicas in different racks, so losing a whole zone loses one copy, not all of them. - Follower fetching: set
replica.selector.class=org.apache.kafka.common.replica.RackAwareReplicaSelectoron brokers andclient.rackon consumers. Consumers then read from a replica in their own zone, cutting cross-zone network cost, which is a large share of cloud Kafka bills. - Writes still go to the leader, wherever it is.
A common cloud layout: one region, three availability zones, replication factor 3 with one replica per zone, min.insync.replicas=2, acks=all. Any one zone can fail with no data loss and writes continuing, and follower fetching keeps consumer traffic inside each zone.
12.9Lab: replication in action
On the three-broker cluster with the k helper, create a topic and write some data:
k kafka-topics.sh --create --topic repl --partitions 1 --replication-factor 3 --config min.insync.replicas=2
k kafka-producer-perf-test.sh --topic repl --num-records 20000 --record-size 200 --throughput -1
k kafka-topics.sh --describe --topic replNote the leader. Stop a follower, produce more, then check the ISR and the end offset. (The commands assume node 3 is a follower; if it is the leader, stop and later start kafka-2 instead.)
docker stop kafka-3
k kafka-producer-perf-test.sh --topic repl --num-records 20000 --record-size 200 --throughput -1
k kafka-topics.sh --describe --topic repl
k kafka-topics.sh --describe --under-min-isr-partitions
k kafka-get-offsets.sh --topic replStart it again and watch it rejoin:
docker start kafka-3
k kafka-topics.sh --describe --topic replCheck what you saw
- With all three running,
Isrlists three brokers. - After stopping a follower,
Isrlists two, writes still succeed (2 meetsmin.insync.replicas=2), and--under-min-isr-partitionsprints nothing because the partition is at, not under, the minimum. kafka-get-offsets.shreturns the latest offset that consumers can read: 40000, the high watermark.- After the restart, the follower first truncates to the leader's history if needed, then fetches the records it missed and reappears in
Isrwithin seconds.
12.10Common mistakes
Common mistakes
- Believing
acks=1is "almost as safe": it loses acknowledged records whenever a leader dies before followers fetch. - Enabling unclean leader election on important topics to "improve availability".
- Replicas in one availability zone, so one zone outage takes all copies.
- Ignoring under-replicated partitions, which means running without the redundancy you planned.
- Leaving leadership skewed after restarts, overloading some brokers.
- Skipping follower fetching in the cloud and paying for cross-zone consumer traffic.
12.11Self-check
- Define LEO and HW, and say which one consumers care about.
Reveal
LEO is the next offset a replica will write; HW is the highest offset every in-sync replica has. Consumers read only below the HW.
- Why does Kafka elect leaders from the ISR?
Reveal
In-sync replicas have every committed record, so electing one of them guarantees no committed record is lost.
- What is a leader epoch for?
Reveal
It numbers each leadership period. Replicas and clients use it to reject stale leaders and to find exactly where a returning replica's log diverged, so it can truncate safely.
- What happens when all in-sync replicas of a partition fail?
Reveal
With unclean election off, the partition is unavailable until an in-sync replica returns (or, with ELR, an eligible replica can be chosen). With it on, a lagging replica becomes leader and committed records it lacks are lost.
- How does follower fetching save money?
Reveal
Consumers read from a replica in their own availability zone instead of the leader's zone, avoiding cross-zone data transfer charges.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 13: Log compaction
Deletion by time keeps recent history; compaction keeps the latest value for every key, forever. That turns a topic into a durable table you can rebuild state from. This module covers what compaction guarantees, tombstones for deletes, how the log cleaner decides what to clean, the settings that control it, and where compacted topics are used.
- A compacted topic receives price updates for AAPL every second for a year. Roughly how many AAPL records remain?
Reveal
At least the latest one, plus whatever has not been cleaned yet (the active segment and the dirty part). Old updates are eventually removed.
- How do you delete a key from a compacted topic?
Reveal
Produce a tombstone: a record with that key and a null value. After cleaning and
delete.retention.ms, the key disappears entirely. - Does compaction change the offsets of the records it keeps?
Reveal
No. Kept records keep their offsets; the log simply has gaps.
- Can you produce records without a key to a compacted topic?
Reveal
No. The broker rejects keyless records on compacted topics, since compaction is per key.
- Name a compacted topic that every Kafka cluster has.
Reveal
__consumer_offsets: only the latest committed offset per group and partition matters.
13.1The idea: keep the latest value per key
For each key, keep at least the most recent record and remove older ones in the background. The topic becomes a snapshot of current state, plus recent changes.
Your phone's contacts versus its call log. The call log keeps every call in order (delete by time); contacts keep only the latest number for each person (compaction).
- Turn it on with
cleanup.policy=compact. Usecleanup.policy=compact,deleteto also drop records older thanretention.ms. - Every record must have a key; the key is what compaction compares.
- A tombstone (key with a null value) means "this key is deleted". It is kept for
delete.retention.ms(1 day) after it is cleaned so consumers can see the delete, then it is removed.
13.2What compaction guarantees
- A consumer that keeps up sees every record. Compaction only touches the older, cleaned part of the log.
- Order is preserved. Compaction removes records; it never reorders them.
- Offsets never change.
- A consumer reading from the beginning sees at least the final value of every key, in write order. It also sees tombstones if it finishes reading within
delete.retention.ms.
It does not promise that only one record per key exists at any moment. Until the cleaner runs, duplicates for a key are normal, so consumers must treat the topic as a sequence of upserts.
13.3How the log cleaner works
- Cleaner threads (
log.cleaner.threads, 1 by default) pick the partition whose log has the highest dirty ratio: the share of the log written since the last cleaning. - A partition is eligible when its dirty ratio exceeds
min.cleanable.dirty.ratio(0.5), or when a record has waited longer thanmax.compaction.lag.ms. - The cleaner builds a map from key to latest offset over the dirty part, then rewrites the older segments keeping only records that are the latest for their key, and swaps the new segments in.
- The active segment is never cleaned. On a low-traffic topic with the default
segment.msof 7 days, stale values can sit in the active segment for a week. Lowersegment.msif that matters.
| Setting | Default | Controls |
|---|---|---|
cleanup.policy |
delete |
compact, delete, or both |
min.cleanable.dirty.ratio |
0.5 | How dirty a log must be before cleaning; lower means more cleaning work and less duplication |
min.compaction.lag.ms |
0 | Minimum age before a record may be compacted away, so consumers can see every update |
max.compaction.lag.ms |
unlimited | Maximum time a record can wait before cleaning; use it to bound how long deleted data lingers |
delete.retention.ms |
1 day | How long tombstones stay after cleaning |
segment.ms |
7 days | How often the active segment rolls and becomes cleanable |
For "right to be forgotten" requests on a compacted topic, a tombstone alone is not enough: set max.compaction.lag.ms so cleaning is guaranteed within your deadline, and remember copies in other places (consumers' databases, backups, mirrors). For data on delete-policy topics, encrypt personal data with a per-person key and delete the key (crypto-shredding).
13.4Where compacted topics are used
| Use | Key | Value |
|---|---|---|
__consumer_offsets |
Group, topic, partition | Committed offset |
| Kafka Streams changelogs and KTables (Module 17) | Table key | Latest aggregate or row |
| Change data capture of a table (Module 16) | Primary key | Latest row, tombstone on delete |
| Reference data (merchants, currency rates, feature flags) | Entity ID | Current record |
| Rebuilding a cache or search index | Entity ID | Current document |
| Latest position of each vehicle or device | Device ID | Latest location |
A service can rebuild its whole state by reading a compacted topic from the beginning, then keep it current by continuing to consume.
Do not add partitions to a compacted topic in place. A key's old value stays in its old partition and its new value lands in a new one, and the two are never compacted together, so a reader can end up with a stale value (Module 6). Tiered storage also does not support compacted topics.
13.5Lab: watch compaction happen
On the single broker, create a compacted topic with settings that make cleaning happen within seconds (lab values only):
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic prices --partitions 1 \
--replication-factor 1 --config cleanup.policy=compact --config segment.ms=5000 \
--config min.cleanable.dirty.ratio=0.01 --config delete.retention.ms=1000
./kafka-console-producer.sh --bootstrap-server localhost:9092 --topic prices \
--reader-property parse.key=true --reader-property key.separator=: \
--reader-property null.marker=NULLType these lines, then wait 10 seconds and type one more line, TICK:1, so the segment rolls:
AAPL:100
MSFT:300
AAPL:101
GOOG:150
AAPL:102
MSFT:301Wait about 30 seconds (the cleaner checks every 15 seconds when idle), then read from the beginning:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic prices --from-beginning \
--formatter-property print.key=true --formatter-property print.offset=trueNow delete GOOG: produce GOOG:NULL, wait 10 seconds, produce TICK:2, wait a minute, and read again.
Check what you saw
- The first read shows
GOOG 150at offset 3,AAPL 102at offset 4,MSFT 301at offset 5, andTICK 1at offset 6. Offsets 0 to 2 are gone; the kept records kept their offsets. - After the tombstone, a read soon after cleaning may show
GOOG null(the tombstone) for a short time; afterdelete.retention.msand another cleaning pass, GOOG disappears entirely. - If you read before the cleaner ran, you would see all records. Compaction is eventual, never immediate.
13.6Common mistakes
Common mistakes
- Assuming exactly one record per key at all times: duplicates exist until cleaning.
- Producing keyless records to a compacted topic (they are rejected).
- Low-traffic compacted topics with the default
segment.ms, leaving stale values in the active segment for days. - Using compaction alone for data deletion deadlines without
max.compaction.lag.ms. - Very low
min.cleanable.dirty.ratioin production, which keeps the cleaner busy rewriting segments. - Growing the partition count of a compacted topic in place.
13.7Self-check
- What four guarantees does compaction give?
Reveal
Caught-up consumers see every record; order is preserved; offsets never change; a consumer reading from the start sees at least the latest value of every key (and tombstones, if it finishes within
delete.retention.ms). - What is a tombstone, and how long does it live?
Reveal
A record with a key and a null value, marking that key deleted. It stays for
delete.retention.msafter the cleaner processes it, then is removed. - When does the cleaner clean a partition?
Reveal
When its dirty ratio exceeds
min.cleanable.dirty.ratio, or when a record has waited longer thanmax.compaction.lag.ms; the active segment is always skipped. - Why use
compact,delete?Reveal
To keep the latest value per key but also drop anything older than
retention.ms, for example windowed aggregates that are useless after some time. - Why is a compacted topic a good way to rebuild a cache?
Reveal
Reading it from the beginning yields the current value of every key without replaying the full history, then live consumption keeps the cache up to date.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 14: Exactly-once semantics and transactions
"Exactly-once" in Kafka has a precise meaning and a precise scope. This module builds it from parts: the idempotent producer, transactions that write to many partitions atomically and commit consumer offsets with them, read_committed consumers, zombie fencing, and then what to do when the effect lives outside Kafka, with the transactional outbox.
- Does Kafka guarantee that a record is delivered exactly once to a consumer?
Reveal
No. Delivery can repeat after crashes and rebalances. Kafka offers exactly-once processing for read-process-write pipelines that stay within Kafka, using transactions.
- A transaction writes to three partitions. Can a read_committed consumer see one partition's records but not the others?
Reveal
Not for long: either all of the transaction's records become visible (commit) or none do (abort). Partitions are read independently, so visibility may arrive a moment apart, but an aborted transaction is never visible anywhere.
- What does
transactional.idgive you that plain idempotence does not?Reveal
Identity across restarts: a restarted instance with the same ID fences its predecessor, and unfinished transactions are completed or aborted, so a zombie cannot write duplicates.
- Your consumer reads Kafka and calls a bank API. Do Kafka transactions make the API call exactly-once?
Reveal
No. Transactions cover Kafka writes and offset commits only. The API call needs an idempotency key on the bank's side.
- What is the transactional outbox for?
Reveal
Writing a database change and publishing an event atomically, by putting the event in an outbox table inside the same database transaction and relaying it to Kafka afterwards.
14.1What exactly-once means here
A Kafka Streams-like job reads payments, computes a fee, and writes fees. It crashes after writing a fee but before committing its input offset. What goes wrong without transactions, and what must be atomic to fix it?
Reveal the answer
On restart it reprocesses the payment and writes a second fee: a duplicate in fees. To fix it, "write the output" and "commit the input offset" must happen atomically: both or neither. That is exactly what a Kafka transaction does.
- The effect happens once, even though records may be delivered more than once internally. People also call it "effectively once".
- Scope: consume from Kafka, process, produce to Kafka, and commit offsets, all inside Kafka. Effects in other systems need idempotency (Modules 8 and 10).
- The parts: the idempotent producer (no duplicates from retries), transactions (atomic multi-partition writes plus offsets), and
read_committedconsumers (never see aborted data).
14.2Transactions
transactional.idnames the producer across restarts. The transaction coordinator for that ID is the broker leading its partition of__transaction_state.initTransactions()gets a producer ID and bumps the epoch, fencing any older instance with the same ID, and finishes or aborts its open transaction.- Inside a transaction the producer writes to any number of partitions, and
sendOffsetsToTransaction()adds the consumed offsets. - Commit is a two-phase protocol run by the coordinator: it records
PREPARE_COMMIT, writes a commit marker (a control record that takes an offset) into every involved partition, then recordsCOMPLETE_COMMIT. Abort is the same with abort markers.
producerProps.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "fee-calculator-" + instanceId);
consumerProps.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
producer.initTransactions();
consumer.subscribe(List.of("payments"));
while (running) {
ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(200));
if (records.isEmpty()) continue;
try {
producer.beginTransaction();
for (ConsumerRecord<String, String> r : records) {
producer.send(new ProducerRecord<>("fees", r.key(), computeFee(r.value())));
}
producer.sendOffsetsToTransaction(nextOffsets(records), consumer.groupMetadata());
producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
producer.close(); // fatal: another instance took over, or we lack permission
throw e;
} catch (KafkaException e) { // includes timeouts and TransactionAbortableException
producer.abortTransaction();
rewindToCommitted(consumer); // seek back so the batch is processed again
}
}nextOffsets(records)returns, for each partition, the last processed offset plus one.consumer.groupMetadata()lets the broker fence a zombie by consumer group generation too, so one transactional producer per application instance is enough.- Since Kafka 4.0, treat a
TimeoutExceptionorTransactionAbortableExceptionduring a transaction as a reason to abort and retry, never to continue.
14.3read_committed consumers
isolation.level=read_committeddelivers committed transactional records and non-transactional records, skips aborted ones (brokers keep an aborted-transactions index per segment), and stops at the last stable offset (LSO).- The default,
read_uncommitted, delivers everything up to the high watermark, including records from transactions that later abort. - An open transaction holds back the LSO for every read_committed consumer of that partition, even for records from other producers written after it. Long transactions mean visible delay.
transaction.timeout.ms(60 s on the producer, at mosttransaction.max.timeout.ms, 15 minutes, on the broker) bounds how long a transaction may stay open before the coordinator aborts it.
14.4Zombie fencing
- Every
initTransactions()call increases the epoch for thattransactional.id. Writes and commits carrying an older epoch are rejected withProducerFencedException. - Choose IDs that are stable per instance and unique across instances, such as the application name plus a pod ordinal. Two live instances sharing an ID keep fencing each other.
- Kafka 4.0 strengthened the protocol (KIP-890): the epoch is bumped on every transaction and brokers verify that a partition belongs to an ongoing transaction before accepting transactional writes. This closes holes that could leave "hanging" transactions.
14.5Exactly-once when a database is involved
The order service must save an order in PostgreSQL and publish OrderPlaced. Why is "commit the database transaction, then call producer.send()" not safe, and why is the reverse order not safe either?
Reveal the answer
DB first: a crash after the commit but before the send loses the event forever. Send first: a crash before the DB commit publishes an event for an order that does not exist. Two separate systems cannot be updated atomically without help; this is the dual-write problem.
- Producer side, transactional outbox: write the business row and an outbox row (event type, key, payload, ID) in one database transaction. A relay publishes outbox rows to Kafka: Debezium's outbox event router reading the write-ahead log (Module 16), or a poller using
SELECT ... FOR UPDATE SKIP LOCKED. The relay is at-least-once, so every event carries an ID. - Consumer side, idempotent consumer: apply the effect and record the event ID in one database transaction (Module 10), or store the consumed offset in that transaction (Module 8).
- End to end: outbox plus idempotent consumers gives exactly-once effects across services, without distributed transactions.
14.6Costs and pitfalls
- Latency: records become visible to read_committed consumers only at commit. Commit every few hundred milliseconds or every few hundred records, not per record.
- Throughput: markers and coordinator round trips cost a little; batching many records per transaction amortizes it.
- Hanging transactions (a producer died mid-transaction on an older version) block the LSO until they time out.
kafka-transactions.sh find-hangingandaborthandle them. - Kafka Streams wraps all of this: set
processing.guarantee=exactly_once_v2(Module 17). Flink uses its own checkpoints plus a transactional Kafka sink.
"How do you guarantee each payment produces exactly one ledger entry?" A strong answer states the scope first: inside Kafka, transactions with read_committed consumers; across a database, an outbox on the producer side and idempotent writes keyed by payment ID on the consumer side; for external APIs, idempotency keys. Then names the costs: latency until commit and an LSO that open transactions hold back.
14.7Lab: see read_committed and the LSO
On the single broker, create a topic and start a transactional load that commits every 5 seconds:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic txlab --partitions 1 --replication-factor 1
./kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic txlab \
--num-records 3000 --record-size 20 --throughput 50 \
--transactional-id perf-tx --transaction-duration-ms 5000While it runs, in two other terminals, start one consumer of each isolation level:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic txlab \
--isolation-level read_uncommitted --formatter-property print.offset=true
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic txlab \
--isolation-level read_committed --formatter-property print.offset=trueIn a fourth terminal, inspect the transaction:
./kafka-transactions.sh --bootstrap-server localhost:9092 list
./kafka-transactions.sh --bootstrap-server localhost:9092 describe --transactional-id perf-txCheck what you saw
- The
read_uncommittedconsumer prints a steady stream, about 50 records per second. - The
read_committedconsumer prints nothing for about 5 seconds, then a burst of about 250 records, then waits again: it can only read up to the last stable offset, which jumps at each commit. - Each commit marker takes an offset, so the offsets printed by either consumer skip a number about every 250 records.
listshowsperf-txwith its coordinator and state;describeshows its producer ID, epoch, state (Ongoing or CompleteCommit), and the partitions in the current transaction.
14.8Common mistakes
Common mistakes
- Promising "exactly-once delivery" to a consumer, or exactly-once effects in external systems from Kafka transactions alone.
- Forgetting
read_committedon consumers of transactional topics, so they read aborted records. - Sharing a
transactional.idbetween live instances, so they fence each other. - Random
transactional.idper start, which loses fencing of the previous instance. - One transaction per record, which multiplies overhead and latency.
- Long-running transactions that hold back the LSO for every read_committed consumer.
- Dual writes (database then Kafka, or the reverse) without an outbox.
14.9Self-check
- Which three mechanisms combine to give exactly-once processing within Kafka?
Reveal
The idempotent producer, transactions (atomic writes across partitions plus consumer offsets via
sendOffsetsToTransaction), and consumers withisolation.level=read_committed. - What is the last stable offset?
Reveal
The first offset belonging to a transaction that is still open in a partition. read_committed consumers cannot read beyond it.
- How does Kafka fence a zombie producer?
Reveal
initTransactions()with the sametransactional.idbumps the epoch; the broker rejects writes and commits with an older epoch, and the zombie getsProducerFencedException. - What do you do on a
KafkaExceptionduring a transaction, and on aProducerFencedException?Reveal
For general errors, abort the transaction and seek back to the last committed offsets to reprocess. For fencing (and other fatal errors), close the producer and stop: another instance owns that ID.
- Why does the outbox pattern need idempotent consumers?
Reveal
The relay is at-least-once: it can publish an outbox row more than once after a crash, so consumers deduplicate by event ID.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 15: Schemas, serialization, and event design
Kafka moves bytes; your teams need contracts. This module covers serialization formats, how a schema registry enforces compatible evolution, the rules for changing a schema safely, how to design events and topics that age well, and what to do with payloads too large for Kafka.
- Does the Kafka broker check that a message's value is valid JSON or Avro?
Reveal
No. Brokers store bytes. Validation happens in producers and consumers, usually with serializers backed by a schema registry.
- With BACKWARD compatibility, which do you upgrade first: producers or consumers?
Reveal
Consumers. A backward-compatible schema can read data written with the previous schema, so new consumers can handle old and new data.
- Is it safe to add a field to an Avro schema?
Reveal
Under BACKWARD compatibility, yes if the new field has a default, so a reader on the new schema can fill it in for old records.
- Why should events carry their own ID?
Reveal
So consumers can deduplicate redeliveries and replays. Offsets are not stable identities if records are ever republished.
- A 50 MB document must go to five services. Send it through Kafka?
Reveal
Store it in object storage and publish a small event with a reference and a checksum (claim check).
15.1Kafka stores bytes
- Producers serialize keys and values into bytes; consumers deserialize them. The broker never inspects them (except to apply compression it was told to use).
- That makes the serializer contract the real API between teams. Without enforcement, one producer change can break every consumer silently, and old records in the log live on for the whole retention period.
15.2Choosing a format
| Format | Size and speed | Schema | Evolution | Typical fit |
|---|---|---|---|---|
| JSON | Large, slower | None built in (JSON Schema optional) | By convention only | Prototypes, low volume, human debugging |
| Avro | Compact binary, fast | Required; field names live in the schema, not the data | Excellent, with defaults and reader/writer schemas | Kafka-centric data platforms, CDC |
| Protobuf | Compact binary, fast | Required; numbered fields | Very good; never reuse field numbers | Polyglot services already using gRPC |
| JSON Schema | Same as JSON | Validated against a schema | Supported by registries | Teams that want JSON plus checks |
15.3Schema registry
- A schema registry (Confluent Schema Registry is the most common; Apicurio and AWS Glue are alternatives) stores versions of each schema under a subject and assigns each schema an ID.
- The default naming, TopicNameStrategy, uses subjects
<topic>-keyand<topic>-value, so a topic carries one value type. RecordNameStrategy and TopicRecordNameStrategy allow several event types in one topic. - Serializers prefix each message with a magic byte and the 4-byte schema ID, so the payload stays compact. Deserializers cache schemas by ID.
- Compatibility checks run when a new version is registered. In production, register schemas through CI and turn off
auto.register.schemasin producers so a code change cannot silently publish a new schema.
15.4Compatibility rules
Your consumers run 40 instances deployed weekly; producers deploy daily. You want to add merchant_category to PaymentAuthorized. Which compatibility mode and rollout order keep everyone working?
Reveal the answer
BACKWARD (the registry default): add merchant_category with a default value, upgrade consumers first if they need the field (new consumers read old and new data), then producers start sending it. Old consumers still work too, because Avro readers ignore fields that are not in their schema.
| Mode | Guarantee | Upgrade first | Allowed changes (Avro) |
|---|---|---|---|
BACKWARD (default) |
New schema reads data written with the previous schema | Consumers | Delete fields; add fields with defaults |
FORWARD |
Previous schema reads data written with the new schema | Producers | Add fields; delete fields that have defaults |
FULL |
Both directions | Either | Add or delete only fields with defaults |
*_TRANSITIVE |
The same, against all earlier versions, not just the last | As above | As above |
NONE |
No checks | Coordinate by hand | Anything |
Safe habits regardless of mode:
- Add optional fields with defaults; never rename a field in place (add the new one, migrate, then remove the old).
- Never change a field's type in an incompatible way (string to int).
- Protobuf: never reuse or renumber field numbers; mark removed numbers and names as
reserved. - For a breaking change, publish a new major version to a new topic (
payments.v2), run both while consumers migrate, then retire the old one.
15.5Designing events
- Name events as facts in the past tense:
PaymentAuthorized,CardBlocked. Commands (AuthorizePayment) usually belong on request paths or command topics with one owner. - Include an envelope:
event_id(UUID, for deduplication),event_type,event_time(when it happened),source,schema_version, and the entity ID that is also the key. Put tracing IDs in headers. - Thin or fat? A thin event (
OrderPaid {order_id}) forces consumers to call back for details, coupling them to the producer's API. A fat event (event-carried state transfer, including the fields consumers need) lets consumers work alone, at the cost of larger messages and a wider contract. Most domain events should carry what their consumers need, without dumping whole database rows. - Domain events, not table rows: raw CDC events expose the producer's internal schema. Publish domain events through an outbox when other teams depend on them; keep raw CDC for data platforms.
- Money and time: amounts as integer minor units or decimal types with currency, never floats; timestamps in UTC with explicit precision.
15.6Designing topics
- One event type per topic is simplest for schemas and access control. Several event types in one topic are needed when a consumer must see them in order for the same entity (account opened, then credited, then closed); use a union schema or RecordNameStrategy.
- Name topics consistently:
<domain>.<entity>.<event>.v<major>, for examplecards.authorization.approved.v1. - Decide per topic: key, partition count, replication factor,
min.insync.replicas, retention or compaction, and who may write and read it. - Ownership: one owning team per topic, documented schema, and an on-call contact.
15.7Large messages
- Defaults cap a record batch at about 1 MB (
message.max.byteson the broker,max.message.bytesper topic,max.request.sizeon the producer). Raising limits is possible but affects broker memory, replication, and consumer fetch sizes. - Claim check: put the object in object storage and send a reference with size and checksum. Consumers fetch it only when needed, and Kafka stays fast.
- Compression helps text-heavy payloads; chunking a large message into many records is rarely worth the complexity.
15.8Lab: design a contract
No cluster needed. On paper or in a file:
- Write an Avro schema for
PaymentAuthorizedwith an envelope (event_id,event_timeas a timestamp,schema_version) and a body (card_id,amount_minoras long,currency,merchant_id). - Choose the key and the topic name, and say why.
- Version 2 adds
merchant_categoryand removes nothing. Write it so it is BACKWARD compatible. - Version 3 renames
merchant_idtomerchant_ref. Explain why that is breaking and plan the migration.
Check what you saw
- Key:
card_id, so a card's authorizations stay in order. A topic name likecards.payment.authorized.v1. - Version 2:
{"name": "merchant_category", "type": ["null", "string"], "default": null}. A union with null as the first branch and a null default is the usual Avro way to make a field optional. - Version 3: a rename is a delete plus an add. Old data has no
merchant_ref, and a reader that requires it fails, unless it has a default, in which case old records silently lose the merchant. Instead, addmerchant_refalongsidemerchant_id, have producers fill both, move consumers tomerchant_ref, then dropmerchant_idin a later version, or publishv2to a new topic.
15.9Common mistakes
Common mistakes
- Free-form JSON across team boundaries with no schema or compatibility check.
- Auto-registering schemas from application code in production.
- Renaming or retyping fields in place.
- Reusing Protobuf field numbers.
- Events without IDs or event times, making deduplication and late-data handling impossible.
- Publishing raw database rows as the public contract other teams depend on.
- Sending large files through Kafka instead of a claim check.
15.10Self-check
- What is in the first 5 bytes of a message written with a Confluent registry serializer?
Reveal
A magic byte (0) followed by the 4-byte schema ID; the encoded payload follows.
- Explain BACKWARD and FORWARD compatibility in one line each.
Reveal
BACKWARD: the new schema can read data written with the previous one (upgrade consumers first). FORWARD: the previous schema can read data written with the new one (upgrade producers first).
- How do you make an Avro field optional?
Reveal
Make its type a union with null first,
["null", "string"], and give it"default": null. - When must several event types share one topic?
Reveal
When a consumer needs them in order for the same entity, because ordering exists only within a partition of one topic.
- What is the claim check pattern?
Reveal
Store a large payload outside Kafka (object storage) and publish an event containing a reference, size, and checksum; consumers fetch the payload when needed.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 16: Kafka Connect and change data capture
Most data enters and leaves Kafka through integrations you should not hand-write. This module covers Kafka Connect's workers, connectors, and tasks, converters and single message transforms, error handling with dead letter queues, and change data capture with Debezium, including the failure modes that bite in production.
- What is the difference between a connector and a task?
Reveal
A connector is the configured integration (for example "copy table orders to Kafka"); tasks are the units that actually copy data in parallel, up to
tasks.max. - Where does a distributed Connect cluster store connector configs and source offsets?
Reveal
In Kafka itself, in three compacted internal topics for configs, offsets, and status.
- What does a converter do?
Reveal
It translates between Connect's internal record format and the bytes in Kafka, for example JSON, Avro, Protobuf, or plain strings.
- Why read the database's write-ahead log for CDC instead of polling
SELECT * WHERE updated_at > ?Reveal
The log captures every change, including deletes and intermediate updates, in commit order, with low latency and no extra query load. Polling misses deletes and can miss updates that share a timestamp.
- A Debezium connector for PostgreSQL is stopped for two days. What can happen to the database?
Reveal
Its replication slot keeps all write-ahead log since the last confirmed position, so the database's disk can fill up.
16.1Why Kafka Connect
- Integration without custom code: configure a connector instead of writing, deploying, and operating a producer or consumer per system.
- Source connectors pull data into Kafka (databases via CDC or JDBC, files, message queues, SaaS APIs). Sink connectors push data out (Elasticsearch, S3, data warehouses, databases).
- The framework handles the hard parts: offsets, scaling across workers, retries, serialization, and rebalancing tasks when workers fail.
16.2Architecture
- Workers are JVM processes that run connectors and tasks. In distributed mode they form a cluster (like a consumer group), share work, and expose a REST API, by default on port 8083. Standalone mode runs one worker with local files, for development.
- Tasks are the unit of parallelism. A JDBC source might use one task per table; a sink connector's tasks share topic partitions like consumers in a group named
connect-<connector name>. - Internal topics (names you choose in the worker config) store connector configs, source offsets, and status. They must be compacted and replicated.
| Piece | Role | Examples |
|---|---|---|
| Converter | Bytes to and from Connect records | JsonConverter, StringConverter, ByteArrayConverter, Avro or Protobuf converters with a registry |
| SMT | Change single records in flight | InsertField, ReplaceField, MaskField, Filter with predicates, RegexRouter, TimestampRouter |
| Connector plugin | Talks to the external system | Debezium, JDBC, S3 sink, Elasticsearch sink |
16.3Running a connector
curl -X PUT -H "Content-Type: application/json" \
http://localhost:8083/connectors/orders-to-search/config -d '{
"connector.class": "io.confluent.connect.elasticsearch.ElasticsearchSinkConnector",
"topics": "shop.public.orders",
"connection.url": "http://elasticsearch:9200",
"tasks.max": "3",
"key.ignore": "false",
"errors.tolerance": "all",
"errors.deadletterqueue.topic.name": "orders-to-search-dlq",
"errors.deadletterqueue.context.headers.enable": "true"
}'
curl http://localhost:8083/connectors/orders-to-search/statusPUT /connectors/<name>/configcreates or updates a connector idempotently, which suits automation. Other endpoints pause, resume, restart failed tasks, and list connector plugins.- Third-party connectors come as plugins on each worker's
plugin.path; check their licenses, as some popular ones are not open source.
16.4Errors and dead letter queues
errors.tolerance=none(default): the task fails on the first bad record and stops until someone restarts it.errors.tolerance=all: skip records that fail in conversion or transformation; witherrors.deadletterqueue.topic.name(sink connectors only), send them to a DLQ topic, with failure details in headers whenerrors.deadletterqueue.context.headers.enable=true.errors.retry.timeoutanderrors.retry.delay.max.mscontrol retries of retriable operations;errors.log.enablelogs failures.- Monitor task state: a
FAILEDtask does nothing until restarted, while the connector may still look present. - Exactly-once source connectors are supported since Kafka 3.3 (
exactly.once.source.support=enabledon workers), for connectors that implement it.
16.5Change data capture with Debezium
- Debezium is a set of open-source Connect source connectors that read database change logs: PostgreSQL logical decoding (
pgoutput), the MySQL binlog, SQL Server and Oracle change logs, MongoDB change streams. - One topic per table, named
<topic.prefix>.<schema>.<table>, keyed by primary key, so all changes to a row stay in order. - Change events carry
before,after,op(ccreate,uupdate,ddelete,rsnapshot read),ts_ms, andsourcemetadata (log position, transaction ID). A delete is followed by a tombstone so compacted topics drop the key. - Initial snapshot: on first start the connector reads existing rows, then switches to streaming changes.
- The outbox event router SMT turns rows from an outbox table into clean domain events on per-aggregate topics (the outbox pattern, Module 14).
PostgreSQL replication slots retain write-ahead log until the connector confirms it. If the connector stops, fails, or falls behind, WAL accumulates and can fill the database disk. Alert on replication slot lag, set max_slot_wal_keep_size on the database as a safety limit, and enable Debezium heartbeats so slots advance on quiet databases.
- Schema changes: Debezium tracks DDL; with a schema registry, compatible column additions flow through; destructive changes need a plan.
- Ordering: per primary key within a table. There is no global order across tables, so consumers joining tables must handle out-of-order arrival.
16.6Connect or your own code?
| Use Connect when | Write a service when |
|---|---|
| A maintained connector exists for the system | The logic is business-specific (validation, enrichment, decisions) |
| The job is moving data with light transforms | You need complex error handling per record |
| You want restarts, scaling, and offsets for free | The external API needs custom flow control or auth |
16.7Lab: file connectors in standalone mode
Open a shell in the single-broker container and prepare a worker and two connector configs:
docker exec -it --workdir /tmp broker sh
cp /opt/kafka/config/connect-standalone.properties /tmp/worker.properties
echo "plugin.path=/opt/kafka/libs/connect-file-4.3.0.jar" >> /tmp/worker.properties
printf 'name=local-file-source\nconnector.class=FileStreamSource\ntasks.max=1\nfile=/tmp/input.txt\ntopic=connect-test\n' > /tmp/source.properties
printf 'name=local-file-sink\nconnector.class=FileStreamSink\ntasks.max=1\nfile=/tmp/output.txt\ntopics=connect-test\n' > /tmp/sink.properties
printf 'first line\nsecond line\n' > /tmp/input.txt
/opt/kafka/bin/connect-standalone.sh /tmp/worker.properties /tmp/source.properties /tmp/sink.propertiesIn a second terminal, append a line, then look at the output file and the topic:
docker exec broker sh -c "echo third line >> /tmp/input.txt"
docker exec broker cat /tmp/output.txt
docker exec broker /opt/kafka/bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic connect-test --from-beginning --max-messages 3Check what you saw
output.txtcontains all three lines within a few seconds: source task to Kafka to sink task.- The topic shows records like
{"schema":{"type":"string","optional":false},"payload":"first line"}because the worker usesJsonConverterwithschemas.enable=true. Withvalue.converter.schemas.enable=falseyou would see just the string. - Stop the worker with Ctrl-C and restart it: it does not re-send the first lines, because it stored the source file position in
/tmp/connect.offsets.
16.8Common mistakes
Common mistakes
errors.tolerance=nonewith no monitoring, so a task fails on one bad record and stops silently.- Under-replicated or non-compacted Connect internal topics.
- Debezium without replication slot lag alerts, risking a full database disk.
- Treating raw CDC events as a public contract for other teams.
- Mismatched converters between the producer side and a sink connector (for example Avro data read with
JsonConverter). - Writing custom producers for integrations a maintained connector already handles.
16.9Self-check
- What do workers, connectors, and tasks each do?
Reveal
Workers are the processes; connectors are configured integrations that plan the work; tasks do the copying in parallel and are spread across workers.
- Which error settings make a sink connector skip bad records into a DLQ?
Reveal
errors.tolerance=allwitherrors.deadletterqueue.topic.name(anderrors.deadletterqueue.context.headers.enable=truefor failure details). - What fields does a Debezium change event carry?
Reveal
beforeandafterrow images,op(c, u, d, r),ts_ms, andsourcemetadata such as the log position and table. - Why is a Debezium topic keyed by primary key?
Reveal
So every change to a row lands in one partition in commit order, and compaction keeps the latest row per key.
- What production risk is specific to PostgreSQL CDC?
Reveal
The replication slot retains write-ahead log while the connector is down or behind, which can fill the database disk.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 17: Kafka Streams, and how it compares to Flink
Stream processing turns events into running answers: totals per merchant per minute, enriched payments, fraud signals. This module teaches Kafka Streams, the Java library that ships with Kafka: streams and tables, topologies and repartitioning, state stores with changelogs, windows and late data, joins, exactly-once, and when Apache Flink is the better tool.
- Does Kafka Streams need its own cluster?
Reveal
No. It is a Java library inside your application. You scale by running more instances, which form a consumer group.
- What is the difference between a KStream and a KTable?
Reveal
A KStream is every event (inserts). A KTable is the latest value per key (upserts), like a changelog applied to a table.
- Why does
selectKeyfollowed bygroupByKeycreate an extra topic?Reveal
Records must be regrouped by the new key across instances, so Streams writes them to an internal repartition topic keyed by the new key and reads them back.
- Where does Kafka Streams keep aggregation state, and how does it survive a crash?
Reveal
In local state stores (RocksDB by default), backed by compacted changelog topics in Kafka. A new owner restores the store from the changelog.
- What must be true of two topics before you can join them as a KStream and a KTable?
Reveal
They must be co-partitioned: same key, same partition count, and the same partitioning (a GlobalKTable avoids this requirement).
17.1What Kafka Streams is
A library that reads topics, transforms and aggregates records with local state, and writes results to topics, with fault tolerance built on Kafka itself.
A spreadsheet that recalculates as rows arrive. Each new row updates totals at once, and the sheet's history is saved to Kafka so a copy can be rebuilt anywhere.
Stateful processing (counts, joins, windows) without operating a separate processing cluster.
- The
application.idis the consumer group ID and the prefix of all internal topics. - Input partitions become tasks; tasks are spread over threads (
num.stream.threads) and instances. Adding instances rebalances tasks automatically. - Everything durable lives in Kafka: input, output, repartition topics, and changelogs.
17.2Streams and tables
| Abstraction | Meaning | Created from |
|---|---|---|
KStream |
Every record is an independent event | builder.stream(topic) |
KTable |
Latest value per key; a record with a null value deletes the key | builder.table(topic) or an aggregation |
GlobalKTable |
A full copy of a table on every instance | builder.globalTable(topic); good for small reference data |
17.3Topologies and repartitioning
Properties props = new Properties();
props.put(StreamsConfig.APPLICATION_ID_CONFIG, "merchant-totals");
props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(StreamsConfig.PROCESSING_GUARANTEE_CONFIG, StreamsConfig.EXACTLY_ONCE_V2);
StreamsBuilder builder = new StreamsBuilder();
KStream<String, Payment> payments =
builder.stream("payments", Consumed.with(Serdes.String(), paymentSerde)); // keyed by card
payments
.filter((cardId, p) -> p.status().equals("APPROVED"))
.selectKey((cardId, p) -> p.merchantId()) // new key: forces a repartition topic
.groupByKey(Grouped.with(Serdes.String(), paymentSerde))
.windowedBy(TimeWindows.ofSizeAndGrace(Duration.ofMinutes(1), Duration.ofSeconds(30)))
.aggregate(() -> 0L,
(merchant, p, total) -> total + p.amountMinor(),
Materialized.with(Serdes.String(), Serdes.Long()))
.toStream()
.map((window, total) -> KeyValue.pair(window.key(), total))
.to("merchant-totals-1m", Produced.with(Serdes.String(), Serdes.Long()));
KafkaStreams streams = new KafkaStreams(builder.build(), props);
Runtime.getRuntime().addShutdownHook(new Thread(streams::close));
streams.start();- Stateless operations:
filter,map,mapValues,flatMap,peek,split(branching),merge. PrefermapValuesovermapwhen the key does not change, so no repartition is needed. - Stateful operations:
count,reduce,aggregate, windowed versions of them, and joins. They need records grouped by key. - Repartitioning: any key change (
selectKey,map,groupBy) before a stateful operation writes records to an internal-repartitiontopic so each key lands in one task. It costs a full extra write and read; design keys to avoid unnecessary ones.
17.4State stores and fault tolerance
- Each stateful task owns a local state store, RocksDB on disk by default (in-memory stores are available), under
state.dir. - Every update is also written to a compacted changelog topic. If an instance dies, its tasks move to another instance, which restores the store from the changelog before processing.
- Restoration can be slow for large state.
num.standby.replicas(0 by default) keeps warm copies on other instances for fast failover. - Interactive queries let your service read its local stores directly (for example "current total for merchant 42"), with metadata to route a query to the instance that owns the key.
17.5Time, windows, and late data
- Event time comes from each record's timestamp by default (a custom
TimestampExtractorcan read it from the payload). - Window types: tumbling (fixed, non-overlapping), hopping (fixed size, overlapping steps), sliding (defined by the time difference between records), and session (activity separated by an inactivity gap).
- Late records: Streams tracks stream time, the largest timestamp seen per task. A window accepts records until stream time passes its end plus the grace period; later records are dropped (and counted in metrics).
- Final results only: windowed aggregates emit updates as records arrive. Use
suppress(untilWindowCloses(...))or the emit-final strategy when downstream needs one result per window.
17.6Joins
| Join | Windowed? | Typical use | Co-partitioning |
|---|---|---|---|
| KStream to KStream | Yes, JoinWindows |
Match a payment with its authorization within 5 minutes | Required |
| KStream to KTable | No: each stream record looks up the table's current value | Enrich payments with card or merchant data | Required |
| KTable to KTable | No | Combine two changelogs; foreign-key joins supported | Required (except foreign-key joins) |
| KStream to GlobalKTable | No | Enrich with small reference data keyed differently | Not required |
17.7Processing guarantees
processing.guarantee=at_least_once(default): fast; failures can duplicate output.processing.guarantee=exactly_once_v2: output records, state store changelogs, and input offsets commit in one Kafka transaction (Module 14). Brokers must be 2.5 or newer.commit.interval.msdefaults to 30 seconds, or 100 ms under exactly-once, which bounds end-to-end latency for downstream read_committed consumers.- Since Kafka 4.2, Streams can use its own broker-side rebalance protocol:
group.protocol=streams.
17.8Kafka Streams, Flink, or a plain consumer
| Kafka Streams | Apache Flink | Plain consumer | |
|---|---|---|---|
| Deployment | Library inside your service | Separate cluster or Kubernetes operator | Library |
| Inputs and outputs | Kafka to Kafka (Connect for the rest) | Many connectors: Kafka, files, databases, CDC | Anything you code |
| State | RocksDB plus Kafka changelogs | RocksDB or heap plus checkpoints to durable storage | Build your own |
| Exactly-once | Kafka transactions | Checkpoints plus transactional sinks | Your design |
| Event time | Stream time and grace periods | Watermarks, rich windows, complex event processing | Build your own |
| Best for | Kafka-centric services with moderate state | Large, multi-source pipelines and heavy event-time logic | Simple per-record work |
Flink is common in payment platforms for status tracking and investigation pipelines: Kafka topics in, keyed state per transaction, side outputs for records that fail parsing, results to databases and alerts. Kafka Streams fits when the logic belongs inside one microservice that already owns the topics. Both rely on the same foundations: keys, partitions, state, event time, and checkpointed offsets.
17.9Lab: run the word count demo
On the single broker, create the topics and start the demo that ships with Kafka:
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic streams-plaintext-input \
--partitions 1 --replication-factor 1
./kafka-topics.sh --bootstrap-server localhost:9092 --create --topic streams-wordcount-output \
--partitions 1 --replication-factor 1 --config cleanup.policy=compact
./kafka-run-class.sh org.apache.kafka.streams.examples.wordcount.WordCountDemoIn a second terminal, read the output with the right deserializers; in a third, type some lines:
./kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic streams-wordcount-output \
--from-beginning --formatter-property print.key=true \
--formatter-property key.deserializer=org.apache.kafka.common.serialization.StringDeserializer \
--formatter-property value.deserializer=org.apache.kafka.common.serialization.LongDeserializer
./kafka-console-producer.sh --bootstrap-server localhost:9092 --topic streams-plaintext-inputType kafka streams rock, then kafka is a log. Finally list the internal topics:
./kafka-topics.sh --bootstrap-server localhost:9092 --list | grep streams-wordcountCheck what you saw
- After the first line the consumer shows
kafka 1,streams 1,rock 1; after the second,kafka 2,is 1,a 1,log 1. Each output record is an update to a table. - The internal topics include a
-repartitiontopic (the demo re-keys records by word) and a-changelogtopic for the counts store. - Stop the demo and start it again: counts continue from where they were, because state was rebuilt from the changelog and the input offsets were committed.
17.10Common mistakes
Common mistakes
- Unnecessary repartitions from using
maporselectKeywhen only the value changes. - Joining topics that are not co-partitioned.
- Huge state with no standbys, so every failover waits for a long restore.
- No grace period thinking: either dropping legitimate late records or holding windows open far too long.
- Expecting one final result per window without
suppressor emit-final. - Changing an
application.idcasually: it resets offsets and state, because it names the group and internal topics.
17.11Self-check
- How does Kafka Streams scale out?
Reveal
Each input partition becomes a task; tasks spread over threads and instances, which share one consumer group (the
application.id). Adding instances moves tasks to them. - What makes state in Kafka Streams fault tolerant?
Reveal
Every state store update is written to a compacted changelog topic; a task's new owner restores the store from it, optionally from a standby replica that is already warm.
- What is the grace period?
Reveal
How long after a window's end, measured in stream time, the window still accepts late records before it closes.
- When can you join a stream with a table without co-partitioning?
Reveal
When the table is a GlobalKTable, which is fully replicated to every instance.
- When would you pick Flink over Kafka Streams?
Reveal
For large or multi-source pipelines, heavy event-time logic with watermarks, sources and sinks beyond Kafka, or when a dedicated processing cluster is acceptable.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 18: Spring for Apache Kafka in practice
Most Java services use Kafka through Spring. This module maps everything you learned onto Spring for Apache Kafka: KafkaTemplate, @KafkaListener and listener containers, how Spring commits offsets, the DefaultErrorHandler and dead letter publishing, non-blocking retries with @RetryableTopic, transactions, testing, and a production configuration you can copy.
- Does Spring's listener container use Kafka's auto-commit by default?
Reveal
No. Unless you set
enable.auto.commityourself, Spring turns it off and commits offsets itself after your listener returns, by default once per batch of records from a poll. - Your
@KafkaListenerthrows an exception. What does Spring do by default?Reveal
The
DefaultErrorHandlerretries the record up to 10 attempts in total with no delay, then logs it and moves on. - What does
concurrency = "3"on a listener mean?Reveal
Three consumer threads, each with its own
KafkaConsumer, in the same group. More than the partition count just leaves threads idle. - Where does
DeadLetterPublishingRecoverersend a failed record by default?Reveal
To
<topic>-dlt, same partition number (Spring Kafka 3.3 and later; older versions used<topic>.DLT). - What does
@RetryableTopicgive you that the default error handler does not?Reveal
Non-blocking retries: failures move to delayed retry topics so the main partition keeps flowing, at the cost of ordering.
18.1The pieces
| Spring piece | Kafka concept | Notes |
|---|---|---|
ProducerFactory, KafkaTemplate |
KafkaProducer |
One shared producer, thread-safe |
ConsumerFactory |
KafkaConsumer settings |
Usually from spring.kafka.consumer.* |
ConcurrentKafkaListenerContainerFactory |
Container settings | Concurrency, ack mode, error handler, batch mode |
@KafkaListener |
A poll loop that calls your method | Runs on the container's consumer thread |
DefaultErrorHandler |
Retry, seek, recover | Blocking retries with backoff, then a recoverer |
KafkaTransactionManager |
Transactions | Wraps listener processing and sends in one Kafka transaction |
Spring Boot auto-configures all of this from spring.kafka.* properties. Define your own beans only when you need to change behavior.
18.2Producing with KafkaTemplate
@Service
public class PaymentEvents {
private final KafkaTemplate<String, PaymentAuthorized> template;
public PaymentEvents(KafkaTemplate<String, PaymentAuthorized> template) {
this.template = template;
}
public CompletableFuture<SendResult<String, PaymentAuthorized>> publish(PaymentAuthorized e) {
return template.send("cards.payment.authorized.v1", e.cardId(), e)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("publish failed for {}", e.eventId(), ex); // decide: retry, outbox, fail
}
});
}
}send()returns aCompletableFuture(Spring Kafka 3.0 and later). The same rules as the plain producer apply: handle failures, do not block on every send in a hot path.- For atomic "database row plus event", use the outbox pattern (Module 14) rather than calling
send()inside a database transaction.
18.3Consuming with @KafkaListener
@Component
public class LedgerListener {
@KafkaListener(topics = "cards.payment.authorized.v1", groupId = "ledger", concurrency = "3")
public void onPayment(PaymentAuthorized event,
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition,
@Header(KafkaHeaders.OFFSET) long offset) {
ledger.applyOnce(event); // idempotent by event.eventId()
}
}- Method parameters can be the converted payload, the whole
ConsumerRecord, headers, anAcknowledgment(manual ack modes), or aListof records for batch listeners. concurrencycreates that many consumer threads; keep it at or below the topic's partition count.- Your method runs on the consumer thread. If it takes longer than
max.poll.interval.msfor a whole poll's worth of records, the consumer is kicked out of the group (Module 9). Lowermax.poll.recordsfor slow listeners.
18.4How Spring commits offsets
AckMode |
Commits | Use |
|---|---|---|
BATCH (default) |
After all records returned by one poll are processed | Good throughput; a failure replays the uncommitted part of the batch |
RECORD |
After each record | Fewer replays, more commit traffic |
TIME, COUNT, COUNT_TIME |
After an interval or a number of records | Tuning |
MANUAL |
When you call acknowledgment.acknowledge(); queued and committed at the end of the poll batch |
You control what counts as done |
MANUAL_IMMEDIATE |
Immediately when you acknowledge | Strict control, more commit traffic |
Spring sets enable.auto.commit=false unless you configure it, so the container's commits are the only ones. Combined with an idempotent listener, this gives at-least-once processing with exactly-once effects.
18.5Error handling: DefaultErrorHandler and dead letters
@Configuration
public class KafkaErrorConfig {
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<Object, Object> template) {
var recoverer = new DeadLetterPublishingRecoverer(template); // -> <topic>-dlt
var backOff = new ExponentialBackOffWithMaxRetries(4); // 4 retries
backOff.setInitialInterval(500);
backOff.setMultiplier(2.0);
backOff.setMaxInterval(5_000);
var handler = new DefaultErrorHandler(recoverer, backOff);
handler.addNotRetryableExceptions(ValidationException.class); // straight to the DLT
return handler;
}
}- Default behavior: 10 delivery attempts with no backoff (
FixedBackOff(0, 9)), then the record is logged and skipped. Always configure a recoverer for anything important. - Retries are blocking: the handler seeks back so the same record is redelivered, and the partition waits. Order is preserved. Keep the total backoff for one record well under
max.poll.interval.ms. - Not retryable by default: deserialization, message conversion, and method argument resolution failures go straight to the recoverer.
- Dead letter records keep the original key and value and gain headers such as
kafka_dlt-original-topic,kafka_dlt-original-partition,kafka_dlt-original-offset, andkafka_dlt-exception-message. - The DLT needs at least as many partitions as the source topic, because the default resolver keeps the same partition number. Or supply your own destination resolver.
- Poison bytes: wrap your deserializer in
ErrorHandlingDeserializer, so a record that cannot be deserialized reaches the error handler (and the DLT) instead of failing insidepoll()forever. - Spring Boot wires a
CommonErrorHandlerbean like the one above into its auto-configured container factory.
18.6Non-blocking retries with @RetryableTopic
@RetryableTopic(attempts = "4", backOff = @BackOff(delay = 1000, multiplier = 2.0),
exclude = ValidationException.class)
@KafkaListener(topics = "orders", groupId = "fulfilment")
public void onOrder(OrderPlaced order) {
fulfilment.handle(order);
}
@DltHandler
public void onDeadLetter(OrderPlaced order, @Header(KafkaHeaders.RECEIVED_TOPIC) String topic) {
alerts.raise("order moved to DLT", order.orderId(), topic);
}- Spring creates the retry topics and the DLT (by default with 1 partition each; set
numPartitions) and listeners for them. Delays work by pausing a retry topic's partition until the record is due. - In Spring Kafka 3.x the attribute is
backoff = @Backoff(...)from Spring Retry; 4.x uses its own@BackOff. - Ordering is lost for retried records. Use blocking retries (the error handler) for ordered streams.
18.7Transactions in Spring
- Set
spring.kafka.producer.transaction-id-prefix; Spring Boot then creates aKafkaTransactionManager, and the listener container runs each poll's processing and its offset commit in a Kafka transaction (consume-transform-produce, Module 14). KafkaTemplate.executeInTransaction(...)runs a block of sends in one transaction outside a listener.- Consumers of transactional topics need
isolation.level: read_committed. - Database work is not part of a Kafka transaction; use the outbox and idempotent consumers for database effects.
18.8A production configuration
spring:
kafka:
bootstrap-servers: kafka-1:9092,kafka-2:9092,kafka-3:9092
producer:
acks: all
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer
properties:
enable.idempotence: true
linger.ms: 10
compression.type: lz4
consumer:
group-id: ledger
auto-offset-reset: earliest
max-poll-records: 200
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
properties:
spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JacksonJsonDeserializer
spring.json.trusted.packages: "com.example.events"
isolation.level: read_committed
listener:
ack-mode: record
concurrency: 3
observation-enabled: true- On Spring Kafka 3.x (Spring Boot 3), use
JsonSerializerandJsonDeserializer; 4.x deprecates them in favor of the Jackson 3 basedJacksonJsonSerializerandJacksonJsonDeserializer. observation-enabledturns on Micrometer observations (tracing context across Kafka hops); Micrometer also exposes the Kafka client metrics.- Add the
DefaultErrorHandlerbean from above, and considergroup.instance.idper pod for static membership (Module 9).
18.9Testing
- Unit tests: call listener methods directly with built objects; mock
KafkaTemplate. - Embedded broker:
spring-kafka-testprovides@EmbeddedKafka, an in-process KRaft broker, for fast integration tests. - Real broker: Testcontainers' Kafka module runs the official
apache/kafkaimage, and Spring Boot's@ServiceConnectionwires its address intospring.kafka.bootstrap-servers. Closer to production, slower to start. - Test the unhappy paths: a poison record reaches the DLT, a duplicate event is applied once, and a rebalance mid-batch does not double-apply effects.
18.10Lab: a Spring Boot consumer with a DLT
Using Spring Boot with spring-kafka against the single broker:
- Configure the
application.ymlabove (single bootstrap serverlocalhost:9092) and create topicspayments(3 partitions) andpayments-dlt(3 partitions). - Write a
@KafkaListenerthat parses a JSON payment and throwsValidationExceptionwhenamountis negative, and aTransientExceptionon a flag field the first two times it sees an event ID. - Add the
DefaultErrorHandlerbean withDeadLetterPublishingRecoverer, exponential backoff, andValidationExceptionas not retryable. - Publish a valid payment, a negative one, a transient one, and a line of invalid JSON with the console producer.
- Read
payments-dltwith--formatter-property print.headers=true.
Check what you saw
- The valid payment is processed once. The transient one is retried with growing delays and then succeeds, and payments behind it on the same partition wait (blocking retries keep order).
- The negative payment and the invalid JSON go straight to
payments-dlt: the first because it is not retryable, the second becauseErrorHandlingDeserializerturned the parse failure into aDeserializationException. - The DLT records carry
kafka_dlt-*headers with the original topic, partition, offset, and exception message. - If you delete
payments-dltand leave topic auto-creation off, publishing to the DLT fails; with too few DLT partitions, records from higher partitions fail to publish. Create DLTs deliberately.
18.11Common mistakes
Common mistakes
- Keeping the default error handler for important data, so failures are logged and skipped after 10 instant attempts.
- A DLT with fewer partitions than the source topic.
concurrencyabove the partition count, expecting more throughput.- Slow listeners with the default
max-poll-records, causing rebalance loops. @RetryableTopicon an ordered stream.- Calling
KafkaTemplate.send()inside a database transaction and assuming both commit together. - Missing
ErrorHandlingDeserializer, so one bad payload stalls a partition.
18.12Self-check
- In the default
BATCHack mode, when are offsets committed?Reveal
After the listener has processed every record returned by one poll; a failure means the uncommitted remainder of that batch is redelivered.
- How do you make validation errors skip retries and go straight to the DLT?
Reveal
Register them with
addNotRetryableExceptionson theDefaultErrorHandler(orexcludethem in@RetryableTopic). - Why must the DLT have at least as many partitions as the source topic by default?
Reveal
DeadLetterPublishingRecoverersends each failed record to the same partition number in the DLT. - Blocking retries or
@RetryableTopicfor account balance events?Reveal
Blocking retries through the
DefaultErrorHandler: they keep per-partition order.@RetryableTopicwould let later events overtake a failed one. - What does
ErrorHandlingDeserializersolve?Reveal
It catches deserialization failures and hands them to the error handler as a
DeserializationException, so a poison payload goes to the DLT instead of blockingpoll().
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 19: Performance, tuning, and capacity planning
Tuning Kafka means choosing where to sit between throughput, latency, durability, and cost, then measuring. This module gives you the knobs that matter on producers, consumers, brokers, and topics, explains where end-to-end latency goes, and shows how to size disks and network for a cluster.
- Does
acks=allcut producer throughput in half?Reveal
No. With batching and several requests in flight,
acks=allmostly adds latency per request; throughput stays high. It matters most for a producer that sends one record at a time synchronously. - Your producer sends small batches at low traffic. Which one setting most improves efficiency?
Reveal
linger.ms: waiting a few milliseconds lets batches fill, which also improves compression. - A consumer group cannot keep up. Name the first two things to check.
Reveal
Whether processing (not Kafka) is the bottleneck, for example a slow database call per record, and whether the group has as many consumers as partitions.
- With
acks=1, can a consumer read a record as soon as the leader writes it?Reveal
No. Consumers read only below the high watermark, so replication to all in-sync replicas is on the consumer's path even when the producer did not wait for it.
- Ingress is 100 MB/s with replication factor 3. How much data do brokers write to disk in total?
Reveal
About 300 MB/s across the cluster, before compression: every byte is stored three times.
19.1Think in trade-offs
- Measure first: know your target (records per second, p99 latency, durability requirement) and today's numbers.
- Change one thing at a time, with realistic payloads, at steady state, for long enough to include GC and segment rolls.
- The usual bottleneck is not the broker: it is per-record processing in consumers, network between zones, or a downstream database.
19.2Producer knobs
| Setting | Default | For throughput | For latency | Notes |
|---|---|---|---|---|
batch.size |
16 KB | 64 KB to 256 KB | Default | Per partition; memory comes from buffer.memory |
linger.ms |
5 ms | 10 to 50 ms | 0 to 5 ms | Low traffic benefits most |
compression.type |
none | lz4 or zstd |
lz4 or none |
Cuts network and disk; costs some CPU |
buffer.memory |
32 MB | Raise with many partitions or bursts | Default | Full buffer blocks send() |
acks |
all | all (keep it) | 1 only if loss is acceptable | Durability first |
max.in.flight.requests.per.connection |
5 | 5 | 5 | Above 5 breaks idempotence |
19.3Consumer knobs
| Setting | Default | For throughput | For latency |
|---|---|---|---|
fetch.min.bytes |
1 | 64 KB to 1 MB | 1 |
fetch.max.wait.ms |
500 | 500 | 100 or less |
max.partition.fetch.bytes |
1 MB | Raise for large records or high-volume partitions | Default |
max.poll.records |
500 | Higher if processing is fast | Lower if processing is slow |
- Processing dominates: batch database writes (one statement for 500 records), cache lookups, and avoid a network call per record.
- Parallelism: consumers up to the partition count; beyond that, the worker-pool pattern (Module 7) or share groups (Module 9).
19.4End-to-end latency
- Producer side:
linger.msand batch filling, then one network hop. - Broker side: append, then replication until the high watermark moves. Replication lag directly delays consumers.
- Consumer side:
fetch.max.wait.mswhen there is little data, then your processing. - Exactly-once adds the transaction commit interval for read_committed consumers (Module 14).
- Low-latency setups use
linger.ms0 to 5, smallfetch.max.wait.ms, fast disks, brokers and clients in the same zone, and lean processing. Kafka commonly reaches end-to-end latencies in the low tens of milliseconds at p99 when tuned for it.
19.5Broker and topic levers
- Partitions spread load over brokers and disks (Module 6). Keep leaders balanced.
- Threads:
num.network.threads(3) accept and answer requests;num.io.threads(8) do the disk work;num.replica.fetchers(1) parallelize follower replication. Raise them when idle percentages (NetworkProcessorAvgIdlePercent,RequestHandlerAvgIdlePercent) stay low. - Memory: a modest JVM heap (about 6 GB is a common starting point) and the rest of RAM for page cache. Consumers that fall far behind read from disk and slow everyone.
- Disks: several data directories (
log.dirs) or RAID 10; separate them from the OS disk; watch fill level, since a full disk takes the broker's partitions offline. - Network: replication multiplies ingress by
RF - 1, and every consumer group multiplies egress. Network is often the first limit, especially across zones. - Compression at the producer, with topic
compression.type=producer, so brokers never recompress.
Host settings that matter. Kafka needs little OS tuning, but these come up on every production checklist:
| Setting | Starting point | Why |
|---|---|---|
| JVM heap | -Xmx6g -Xms6g with G1 (the Kafka docs' example) |
Kafka keeps data in the page cache, not the heap; a huge heap starves the cache and lengthens GC pauses |
| File descriptors | at least 100,000 | Every segment file and client connection holds one |
vm.max_map_count |
above the Linux default (about 65,530) for many partitions | Each segment's offset and time index is memory-mapped: about 2 map areas per segment |
| Filesystem | XFS or ext4, mounted with noatime |
Kafka never reads access times, so updating them on every read is wasted writes |
vm.swappiness |
low, often 1 (common practice) | A swapped-out broker pauses long enough to be fenced and trigger leader elections |
| Data disks | dedicated to Kafka, not shared with OS logs | Keeps append latency steady |
19.6Sizing a cluster
- Storage = ingress per day x retention days x replication factor x compressed ratio, plus headroom (30 to 50 percent free for growth, reassignments, and failures).
- Network in per broker = (ingress x RF) / brokers. Network out = consumer groups x ingress + replication.
- Failure headroom: the cluster must carry full load with one broker (or one zone) down, so size for N - 1.
- Partitions per broker in the low thousands are normal; much higher counts need testing.
19.7Benchmarking
# producer: 1 KB records as fast as possible with batching and compression
kafka-producer-perf-test.sh --bootstrap-server localhost:9092 --topic perf \
--num-records 2000000 --record-size 1024 --throughput -1 \
--command-property acks=all --command-property linger.ms=20 \
--command-property batch.size=131072 --command-property compression.type=lz4
# consumer: read the same records back
kafka-consumer-perf-test.sh --bootstrap-server localhost:9092 --topic perf \
--num-records 2000000 --group perf-reader- Use payloads that look like production (real JSON compresses; random bytes do not).
- Report records/s, MB/s, and p99 latency, and watch broker CPU, disk, and network at the same time.
- Run long enough to fill the page cache and roll segments; first-minute numbers flatter.
19.8Lab: acks and consumer throughput
On the three-broker cluster with the k helper:
k kafka-topics.sh --create --topic bench --partitions 6 --replication-factor 3 --config min.insync.replicas=2
k kafka-producer-perf-test.sh --topic bench --num-records 500000 --record-size 512 --throughput -1 \
--command-property acks=1 --command-property linger.ms=10
k kafka-producer-perf-test.sh --topic bench --num-records 500000 --record-size 512 --throughput -1 \
--command-property acks=all --command-property linger.ms=10
k kafka-consumer-perf-test.sh --topic bench --num-records 1000000 --group bench-readerCheck what you saw
acks=allusually shows a higher average and p99 latency thanacks=1, while records per second stay in the same range, because batching hides most of the extra round trip.- On a single laptop all three brokers share one disk and CPU, so differences are smaller and noisier than on real hardware.
- The consumer test usually reports much higher throughput than the producer: consumers read sequentially from the page cache with no replication on their path.
19.9Common mistakes
Common mistakes
- Tuning without a target or without measuring before and after.
- Lowering durability (
acks=1) for throughput that batching would have delivered safely. - Giving brokers huge heaps, starving the page cache.
- Forgetting replication and consumer fan-out when sizing network.
- Sizing for average load instead of peak, and for all brokers up instead of one down.
- Benchmarking with random bytes and expecting compression gains.
19.10Self-check
- Name three producer settings you would change for higher throughput.
Reveal
Larger
batch.size, higherlinger.ms, andcompression.typeoflz4orzstd(and enoughbuffer.memory). - Why does replication delay consumers even with
acks=1?Reveal
Consumers read only below the high watermark, which advances only when all in-sync replicas have the records.
- How do you compute total disk for a topic?
Reveal
Ingress per day times retention days times replication factor times the compressed size ratio, plus 30 to 50 percent headroom.
- Broker
RequestHandlerAvgIdlePercentis near 0. What does it mean?Reveal
The I/O request threads are saturated; requests queue. Look for disk pressure, too many small requests, or raise
num.io.threads. - Why size for N - 1 brokers?
Reveal
When a broker fails, its partitions' load moves to the others; the cluster must handle full traffic without it.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 20: Operating Kafka: monitoring, changes, security, and disaster recovery
Running Kafka well is mostly watching the right numbers and changing the cluster carefully. This module covers the metrics that matter, consumer lag done properly, everyday changes, partition reassignment, rolling upgrades, encryption, authentication, ACLs, quotas, and cross-region replication with MirrorMaker 2.
- Which broker metric best tells you the cluster has lost redundancy?
Reveal
UnderReplicatedPartitionsabove zero (andUnderMinIsrPartitionCountabove zero is worse:acks=allwrites to those partitions are failing). - Across all controllers, what should
ActiveControllerCountadd up to?Reveal
Exactly 1. Zero means no active controller; more than one indicates a serious problem.
- You add a fourth broker. Does Kafka move partitions to it automatically?
Reveal
No. New topics may use it, but existing partitions stay put until you run a reassignment (or a tool like Cruise Control does).
- Name the three layers of Kafka security.
Reveal
Encryption in transit (TLS), authentication (mTLS or SASL), and authorization (ACLs checked per request).
- MirrorMaker 2 copies topic
ordersfrom clustereasttowest. What is the topic called inwestby default?Reveal
east.orders: the default replication policy prefixes the source cluster alias.
20.1What to monitor
| Metric | Healthy | Alert when | Usually means |
|---|---|---|---|
UnderReplicatedPartitions |
0 | Above 0 for minutes | A broker is down, slow, or its disk or network is saturated |
UnderMinIsrPartitionCount |
0 | Above 0 | acks=all writes to those partitions are failing |
OfflinePartitionsCount (controller) |
0 | Above 0 | Partitions with no leader: unavailable |
ActiveControllerCount (sum) |
1 | Not 1 | Controller quorum problem |
RequestHandlerAvgIdlePercent |
Well above 0.3 | Below 0.2 | I/O threads saturated |
NetworkProcessorAvgIdlePercent |
Well above 0.3 | Below 0.2 | Network threads saturated |
IsrShrinksPerSec, IsrExpandsPerSec |
Near 0 | Flapping | Followers struggling to keep up |
TotalTimeMs (Produce, Fetch) p99 |
Stable | Rising | Overload, slow disks, GC |
Producer record-error-rate |
0 | Above 0 | Sends failing after retries |
| Consumer lag | Small, stable | Growing, or high in time | Consumers too slow or stuck |
Also watch disk usage (alert well before full), network throughput against capacity, CPU, GC pauses, and open file descriptors.
20.2Consumer lag done right
- Measure from outside the consumer: an exporter (for example kafka-lag-exporter or Burrow) or the Admin API compares each group's committed offsets with partition end offsets. A dead consumer reports no metrics of its own.
- Alert on lag in time ("oldest unprocessed record is 6 minutes old") and on growth trend, not on a fixed message count that means different things for different topics.
- Per partition: one stuck partition hides inside a healthy total (poison message, hot key).
- Compare with retention: lag in time approaching retention means imminent data loss.
20.3Everyday changes
# change a topic config at runtime (no restart)
kafka-configs.sh --bootstrap-server localhost:9092 --alter --entity-type topics \
--entity-name payments --add-config retention.ms=1209600000
kafka-configs.sh --bootstrap-server localhost:9092 --describe --entity-type topics --entity-name payments
# add partitions (keyed topics: read Module 6 first)
kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic payments --partitions 24
# purge records before an offset in partition 0 (moves the log start offset)
echo '{"partitions":[{"topic":"payments","partition":0,"offset":1000}],"version":1}' > purge.json
kafka-delete-records.sh --bootstrap-server localhost:9092 --offset-json-file purge.json- Many broker configs are dynamic: change them with
kafka-configs.sh --entity-type brokerswithout restarting. - Keep topic configuration in code (Terraform, a GitOps repo, or an operator such as Strimzi) so changes are reviewed and repeatable.
20.4Partition reassignment and adding brokers
# 1. let Kafka propose a plan for these topics across brokers 1-4
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--topics-to-move-json-file topics.json --broker-list 1,2,3,4 --generate
# 2. save the proposed plan as plan.json, then execute it with a replication throttle (bytes/s)
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--reassignment-json-file plan.json --execute --throttle 50000000
# 3. check progress; once complete, verify also removes the throttle
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 \
--reassignment-json-file plan.json --verify- Always throttle large moves so replication traffic does not starve producers and consumers.
- Save the current assignment (printed by
--generate) as a rollback plan. - Tools such as Cruise Control automate balancing by disk, network, and leader counts.
- Removing a broker: reassign its replicas away first, then shut it down.
20.5Rolling restarts and upgrades
- One broker at a time: stop it cleanly (controlled shutdown moves leadership away), restart, and wait until
UnderReplicatedPartitionsreturns to 0 before the next. - Upgrades in KRaft: roll every node onto the new software, verify behavior, then finalize with
kafka-features.sh --bootstrap-server ... upgrade --release-version 4.3. Some metadata versions cannot be downgraded after finalizing, so test first. - Clients: Kafka keeps clients and brokers compatible across versions in both directions, with a floor: 4.x clients and brokers need the other side to be at least 2.1.
- Controllers in a dedicated quorum are rolled one at a time too, keeping a majority up.
20.6Security
| Layer | Options | Notes |
|---|---|---|
| Encryption | TLS (SSL or SASL_SSL listeners) |
Encrypts client and inter-broker traffic; disables zero-copy |
| Authentication | mTLS client certificates; SASL/SCRAM-SHA-512; SASL/OAUTHBEARER (OIDC); SASL/GSSAPI (Kerberos); SASL/PLAIN (only over TLS) | Produces a principal such as User:ledger-service |
| Authorization | ACLs with the KRaft StandardAuthorizer |
Allow or deny an operation on a topic, group, cluster, or transactional ID |
| Quotas | producer_byte_rate, consumer_byte_rate, request_percentage |
Per user or client ID; protect the cluster from noisy tenants. A client over its quota is not failed: the broker answers with a throttle time and mutes that connection for that long, so the client slows down |
kafka-acls.sh --bootstrap-server localhost:9092 --command-config admin.properties --add \
--allow-principal User:ledger-service --operation Read --topic payments --group ledger
kafka-acls.sh --bootstrap-server localhost:9092 --command-config admin.properties --add \
--allow-principal User:payments-api --producer --topic payments
kafka-configs.sh --bootstrap-server localhost:9092 --command-config admin.properties --alter \
--entity-type users --entity-name payments-api --add-config producer_byte_rate=10485760--producerand--consumerare shortcuts that grant the operations a producer or consumer needs (Write, Describe, and Create for producers; Read and Describe on the topic plus Read on the group for consumers).- Use prefixed resource patterns (
--resource-pattern-type prefixed) to grant a team all topics starting withpayments.. - Rotate credentials, never share one principal across services, and audit who can create or delete topics.
20.7Multi-cluster and disaster recovery
- Within a region, a cluster stretched across three availability zones (Module 12) survives a zone loss with no data loss. Most teams stop here.
- Across regions, latency makes synchronous replication impractical, so a second cluster receives an asynchronous copy:
- MirrorMaker 2, built on Kafka Connect, replicates topics, configs, and ACLs, and emits checkpoints that translate consumer group offsets between clusters.
- Active-passive: producers write to one region; on failover, consumers start in the backup region from translated offsets. Expect some duplicates and a small loss window equal to replication lag (RPO).
- Active-active: each region writes locally; topics are mirrored both ways under prefixed names so loops are impossible. Consumers read local and remote topics; designs must tolerate the same entity being updated in two regions.
- Offsets differ between clusters, which is why translation is needed; never copy raw offset numbers across. MirrorMaker 2 emits checkpoints every 60 seconds by default; with
sync.group.offsets.enabled=true(off by default) it also writes the translated offsets into the target cluster's__consumer_offsetsfor groups that are not running there, so standby consumers can simply start. - Commercial alternatives (for example Confluent Cluster Linking) preserve offsets byte for byte.
| Topology | Write latency | Data loss if a region dies (RPO) | Offsets | Use when |
|---|---|---|---|---|
| One cluster across 3 zones | About 1 to 2 ms | None for a zone loss; a region loss takes the cluster down | One space | The default for most systems |
| Stretch cluster across 3 regions | Adds cross-region round trips to every acks=all write (tens of ms) |
None, with replicas and controllers in 3 regions | One space | Rare: zero loss across regions is worth the latency |
| Two clusters, active-passive (MM2) | Local | Seconds of replication lag | Translated | Regional disaster recovery |
| Two clusters, active-active (MM2) | Local in each region | Seconds for the other region's data | Translated, prefixed topics | Users write in their own region |
- A stretch cluster needs three locations, not two. The KRaft controllers need a majority. With voters in only two regions, one region must hold the majority, and losing that region stops the quorum. A third region (or a small witness site holding a controller) removes that single point of failure.
Go deeperFailing over and failing back with MirrorMaker 2
Failover (primary region lost):
- Stop or fence producers in the failed region if they are still partly up, so two regions do not accept writes for the same data.
- Point producers at the backup cluster (DNS or config). In active-passive with the default replication policy, they now write to local topics such as
orders, while the mirrored history sits ineast.orders; consumers subscribe to both (or a pattern) until the old topic drains. For one-way replication,IdentityReplicationPolicykeeps topic names identical instead, which simplifies clients but cannot be used for active-active. - Start consumers in the backup region from translated offsets (synced group offsets, or
RemoteClusterUtils.translateOffsets). Translation can land slightly early, so expect some duplicates; idempotent consumers absorb them. - Accept that records still in flight when the region died (the replication lag) may be missing until the old region returns; reconcile critical data afterwards.
Failback (primary region back):
- Do not switch producers back yet: the backup has writes the primary never saw.
- Run MirrorMaker 2 in the reverse direction, wait until lag reaches zero and checkpoints are current.
- Switch producers back in a quiet window, then consumers using translated offsets, then restore the original replication direction.
- Rehearse both directions regularly; an untested runbook is the most common failure.
20.8Managed Kafka
Managed services (Confluent Cloud, Amazon MSK, Aiven, and others) run brokers, upgrades, and disks for you. You still own topic design, keys and partitions, client configuration, schemas, security policies, consumer lag, and cost (especially cross-zone traffic).
20.9Lab: change replication with a reassignment
On the three-broker cluster:
k kafka-topics.sh --create --topic grow --partitions 3 --replication-factor 2
k kafka-topics.sh --describe --topic grow
printf '%s' '{"version":1,"partitions":[{"topic":"grow","partition":0,"replicas":[1,2,3]},{"topic":"grow","partition":1,"replicas":[2,3,1]},{"topic":"grow","partition":2,"replicas":[3,1,2]}]}' \
| docker exec -i kafka-1 sh -c 'cat > /tmp/plan.json'
k kafka-reassign-partitions.sh --reassignment-json-file /tmp/plan.json --execute --throttle 10000000
k kafka-reassign-partitions.sh --reassignment-json-file /tmp/plan.json --verify
k kafka-topics.sh --describe --topic growThen change a config at runtime and read it back:
k kafka-configs.sh --alter --entity-type topics --entity-name grow --add-config retention.ms=86400000
k kafka-configs.sh --describe --entity-type topics --entity-name growCheck what you saw
- Before: each partition lists two replicas.
--executeprints the current assignment (save it as a rollback plan) and starts the move. --verifyreports each partition's reassignment as complete and clears the throttle;describenow shows three replicas and three ISR members per partition, with the first replica as preferred leader.- The topic stayed readable and writable throughout: replicas were added, caught up, then joined the ISR.
--describeon configs showsretention.ms=86400000as a dynamic topic config.
20.10Common mistakes
Common mistakes
- Alerting on lag in messages only, or measuring lag from inside the consumer that is stuck.
- Unthrottled reassignments that saturate the network in business hours.
- Restarting the next broker before under-replicated partitions return to zero.
- PLAINTEXT listeners in production, or SASL/PLAIN without TLS.
- One shared principal for many services, which makes ACLs and audits meaningless.
- Copying consumer offsets as numbers between clusters instead of translating them.
- No tested failover runbook for the second region.
20.11Self-check
- What is the difference between UnderReplicatedPartitions and UnderMinIsrPartitionCount?
Reveal
Under-replicated means some replicas are out of the ISR (less redundancy); under min ISR means the ISR is below
min.insync.replicas, soacks=allwrites to those partitions fail. - Why measure consumer lag externally?
Reveal
A crashed or hung consumer stops reporting its own metrics; an external checker compares committed offsets with end offsets regardless.
- What are the three steps of a partition reassignment?
Reveal
Generate (or write) a plan, execute it with a throttle, and verify until complete (which also removes the throttle).
- What does finalizing an upgrade with
kafka-features.shdo?Reveal
It raises the cluster's metadata version so new features take effect, after all nodes run the new software; some versions cannot be downgraded after this.
- Why can't a stretch cluster across two regions survive the loss of either region on its own?
Reveal
The KRaft controller quorum needs a majority. With voters in two regions, one region holds the majority, and losing it stops the quorum; a third location (a witness) fixes that.
- Why does MirrorMaker 2 need offset checkpoints?
Reveal
The same record has different offsets in the source and target clusters, so consumer positions must be translated to resume correctly after failover.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 21: Troubleshooting playbook
When Kafka misbehaves at 2 AM you need a method, not a search engine. This module is a playbook: a general approach, then symptom by symptom (lag, rebalance loops, duplicates, missing data, producer errors, broker trouble, ordering, connectivity) with checks, causes, and fixes, and a set of incident drills to practice on.
- Lag grows on one partition only. What are the two most likely causes?
Reveal
A poison record that the consumer keeps failing on, or a hot key sending far more traffic to that partition.
- Consumers log
CommitFailedExceptionand the group keeps rebalancing. What is usually wrong?Reveal
Processing a batch takes longer than
max.poll.interval.ms, so members are removed mid-batch and their commits are rejected. - A new consumer group "lost" all historical messages. What is the usual reason?
Reveal
It started with the default
auto.offset.reset=latestand so began at the end of each partition. - Producers report
NotEnoughReplicasException. What does that tell you?Reveal
The partition's ISR is smaller than
min.insync.replicas, usually because brokers are down or followers are lagging. - Clients connect for bootstrap and then time out connecting to
kafka-2:9092. Where do you look?Reveal
At
advertised.listenerson the brokers: the advertised address is not reachable from the clients.
21.1A method for any incident
- Scope it: which topics, partitions, consumer groups, clients, brokers? All or some? Since when?
- What changed? Deploys, config changes, traffic spikes, broker restarts, certificate expiry, a new consumer group.
- Read metrics by layer: producer errors and latency; broker URP, under-min-ISR, offline partitions, idle ratios; consumer lag per partition and rebalance rate.
- Read logs near the start time: client exceptions, broker
server.logandstate-change.log, controller logs. - Form one hypothesis, test it, fix, and watch the metric recover.
- Prevent it: an alert, a config change, a runbook entry.
21.2Consumer lag keeps growing
| Check | Command or signal | If yes |
|---|---|---|
| Lag on one or a few partitions only? | kafka-consumer-groups.sh --describe --group G |
Poison record (same offset for minutes) or hot key (high bytes in on that partition) |
| Fewer active consumers than partitions? | --describe --members |
Scale out, or find crashed instances |
| Rebalancing often? | Consumer logs, rebalance-rate-per-hour |
Fix poll interval, static membership, cooperative assignor |
| Produce rate jumped? | Broker MessagesInPerSec per topic |
Scale consumers (up to partitions); add partitions for the future |
| Processing slower? | Consumer processing time, downstream latency | Fix the downstream, batch writes, add caching |
| Consumers healthy but fetches slow? | Broker TotalTimeMs for FetchConsumer |
Broker overload, cold reads from disk, network |
21.3Rebalance loops and CommitFailedException
- Symptom: logs show members leaving and rejoining,
CommitFailedException, and the same records processed repeatedly. - Causes: processing per poll longer than
max.poll.interval.ms; long GC pauses or CPU throttling (containers with low CPU limits) causing missed heartbeats; pods restarted repeatedly by failing health checks; frequent deploys with an eager assignor. - Fixes: lower
max.poll.records, speed up or move slow work, raisemax.poll.interval.msif a batch legitimately takes long; give consumers enough CPU; use static membership and cooperative rebalancing (orgroup.protocol=consumer); make liveness probes independent of Kafka processing.
21.4Duplicates downstream
- Expected sources (at-least-once): crash or rebalance between processing and commit; producer retries without idempotence; application-level resends; replays and offset resets; outbox relays.
- Checks: do duplicates share an event ID (redelivery) or have different IDs (the producer created two events)? Did a rebalance or deploy happen at that time?
- Fixes: idempotent consumers keyed by event ID (Module 10); idempotent producer on (default); commit after processing; transactions for Kafka-to-Kafka pipelines (Module 14).
21.5Missing or lost messages
Go through this list in order; most "lost" messages were never lost.
| Cause | How to confirm | Fix |
|---|---|---|
New group started at latest |
Group's first committed offsets equal end offsets at start time | auto.offset.reset=earliest, or reset offsets by time |
| Committed offsets expired (group idle more than 7 days) | Group had no members for a long time | Longer offsets.retention.minutes, or reset deliberately |
| Records expired by retention before being read | Lag in time exceeded retention.ms |
Longer retention, faster consumers, lag alerts in time |
Consumer is read_committed and transactions aborted or are open |
Producer uses transactions | Expected behavior; check the producer's transaction errors |
| Producer send failed and the error was ignored | Producer record-error-rate, logs |
Handle callback errors; outbox |
acks=1 (or 0) and a leader failed |
Broker failover at that time | acks=all, min.insync.replicas=2 |
| Unclean leader election | UncleanLeaderElectionsPerSec above 0 |
Disable unclean election on important topics |
| Auto-commit with asynchronous processing | Offsets committed before workers finished | Commit only completed work |
| Consumer filtered or mis-deserialized records | Application logs, DLT | Fix logic; route failures to a DLT |
21.6Producer errors
| Error | Meaning | Fix |
|---|---|---|
TimeoutException: Expiring N record(s) |
Records waited longer than delivery.timeout.ms |
Brokers unreachable or slow; check leaders, network, broker load |
TimeoutException on metadata after max.block.ms |
Cannot fetch metadata or topic missing | Wrong bootstrap address, topic does not exist and auto-create is off, ACLs |
NotEnoughReplicasException |
ISR below min.insync.replicas |
Restore brokers or followers; check disks and network |
RecordTooLargeException |
Record bigger than max.request.size or the topic's max.message.bytes |
Shrink payloads (claim check) or raise limits consistently |
TopicAuthorizationException |
Principal lacks Write or Describe on the topic | Add ACLs |
ProducerFencedException |
Another instance with the same transactional.id started |
Expected after a failover; make IDs unique per live instance |
BufferExhaustedException |
buffer.memory full for max.block.ms |
Producer faster than the cluster accepts: fix broker side, raise buffer, or slow down |
21.7Broker and cluster problems
- Under-replicated partitions: find which brokers host the lagging replicas. Usual causes: a broker down or restarting, disk or network saturation, a slow disk, or too few
num.replica.fetchers. Checkkafka-topics.sh --describe --under-replicated-partitions. - Offline partitions: no in-sync replica is available. Bring back the brokers that hold them; consider unclean election only for data you can afford to lose.
- Disk full: the broker marks the affected log directory offline (a broker with a single log directory shuts down). Free space by lowering retention, deleting unneeded topics, or adding disks, and alert at 70 to 80 percent next time.
- Controller trouble:
ActiveControllerCountnot summing to 1, or metadata changes stalling. Check controller quorum health withkafka-metadata-quorum.sh describe --statusand keep a majority of controllers up. - Hot broker: uneven leaders or partitions. Run a preferred leader election, then rebalance replicas with a reassignment or Cruise Control.
21.8Ordering surprises
- Different partitions: the producer used no key, a different key, or a different partitioner (Java versus librdkafka defaults).
- Partition count changed: keys remapped (Module 6).
- Retries reordering: idempotence turned off with more than one request in flight (Module 5).
- Consumer-side parallelism: a worker pool without per-key routing, or retry topics overtaking later records.
21.9Connectivity and configuration
| Symptom | Likely cause |
|---|---|
| Bootstrap works, then "unknown host" or timeouts to specific brokers | advertised.listeners not reachable from the client network |
SSL handshake failed |
Wrong truststore, expired certificate, hostname mismatch |
SaslAuthenticationException |
Wrong credentials or mechanism mismatch between client and listener |
UnsupportedVersionException |
Client or broker too old for a feature; 4.x needs the other side at 2.1 or newer |
| Works locally, fails in Kubernetes | Clients cannot reach individual broker addresses; only bootstrap is exposed |
21.10Incident drills
Guided designSix incidents to practiceAnswer each before revealing
- 1Black Friday lag
Checkout events lag reaches 40 minutes on every partition of a 12-partition topic. The group has 12 consumers, CPU is low, and the downstream payments database shows high latency.
Write your answer, then reveal
Consumers are waiting on the database, so scaling consumers (already at the partition count) will not help and would add load. Batch writes per poll, add a connection pool or caching, shed non-critical work, and if the database is the hard limit, slow producers or accept lag. Long term: more partitions plus a faster sink, and lag-in-time alerts against retention.
- 2The partition that never moves
Lag grows only on partition 7 of
orders; the committed offset there has been 88213 for an hour. Other partitions are fine.Write your answer, then reveal
Classic poison record at offset 88213. Read it with
kafka-console-consumer.sh --partition 7 --offset 88213 --max-messages 1, check consumer logs for the exception, then route it to a DLT (or skip by resetting that partition's offset to 88214 with the group stopped). Add bounded retries and a DLT so it cannot recur. - 3Rebalance storm after a deploy
After a release that added a slow fraud check, the group rebalances every few minutes and processes the same records again and again.
Write your answer, then reveal
Each poll's batch now takes longer than
max.poll.interval.ms, so members are removed mid-batch. Lowermax.poll.records, make the fraud check faster or asynchronous with proper offset tracking, and raisemax.poll.interval.msonly if needed. Cooperative rebalancing and static membership reduce the blast radius. - 4The new service saw nothing
A new analytics service deployed yesterday shows no data before yesterday, though the topic keeps 30 days.
Write your answer, then reveal
Its consumer group started with the default
auto.offset.reset=latest. Stop the group and reset offsets:--reset-offsets --to-earliest(or--to-datetime) with--execute, then restart. Make sure processing is idempotent before replaying 30 days. - 5NotEnoughReplicas at 3 AM
Producers to
ledgerfail withNotEnoughReplicasException; one broker is down for disk replacement and another just restarted.Write your answer, then reveal
With RF 3 and
min.insync.replicas=2, one broker down is fine, but the restarted broker has not rejoined the ISR yet, leaving one in-sync replica. Writes correctly refuse. Wait for the restarted broker to catch up (watch URP fall), and in future never take a second broker down until URP is zero. Do not lowermin.insync.replicasin a panic. - 6Duplicate emails
Customers receive some payment emails twice. The duplicates have identical
event_idvalues and occurred during a deploy.Write your answer, then reveal
Same event ID means redelivery, not double publishing: the deploy caused a rebalance, and records processed but not yet committed were processed again by the new owner. Make the email consumer idempotent by event ID (a processed-events table, or the email provider's idempotency key), commit in
onPartitionsRevoked, and use cooperative rebalancing to shrink the window.
21.11Common mistakes
Common mistakes
- Scaling consumers beyond the partition count to fix lag.
- Lowering
min.insync.replicasor enabling unclean election during an incident and forgetting to revert it. - Resetting offsets without checking idempotency of downstream effects.
- Restarting brokers one after another without waiting for under-replicated partitions to clear.
- Treating every "missing message" report as data loss before checking offsets, retention, and isolation level.
- No runbook: rediscovering the same fix in every incident.
21.12Self-check
- What is the first question to ask about growing lag?
Reveal
Is it on all partitions or only some? Some points to a poison record or hot key; all points to more input, slower processing, too few consumers, or rebalances.
- How do you tell a redelivery from a double publish?
Reveal
Compare event IDs: identical IDs mean the same event was delivered twice; different IDs mean the producer created two events.
- List three causes of "missing" messages that are not data loss.
Reveal
A new group starting at
latest, expired committed offsets, and a read_committed consumer hiding aborted or open transactions. (Also consumer-side filtering or deserialization failures.) - What does
UnsupportedVersionExceptionusually mean?Reveal
A client and broker version combination lacks a needed API version, for example a client too old for a 4.x broker (which needs 2.1 or newer).
- During an incident, why not just enable unclean leader election to bring partitions back?
Reveal
It can elect a replica that is missing committed records, permanently losing acknowledged data. Use it only for data you can afford to lose, and document the decision.
Mastery checklist
Tick an item only when it is true with your notes closed.
Case study 1: Order events with the outbox pattern
An e-commerce order service must tell payments, inventory, shipping, notifications, and analytics about every order change, reliably and in order per order, without distributed transactions and without slowing down checkout.
Design how the order service publishes order events to the rest of the company. Cover: how the database write and the event stay consistent, topic and key choice, ordering, consumers that must not double-charge or double-ship, adding a new consumer next quarter, and schema changes.
Reveal the answer
A strong answer covers: the dual-write problem and the transactional outbox (or CDC); Debezium or a poller as the relay; one topic of order events keyed by order_id for per-order order; a consumer group per downstream service; idempotent consumers keyed by event_id; a schema registry with BACKWARD compatibility; retention long enough for a new consumer to backfill or a snapshot topic; DLTs per consumer; monitoring of outbox lag and replication slot growth.
C1.1Requirements
Functional
- Publish an event for each order state change: created, paid, packed, shipped, delivered, cancelled.
- Five consumers today (payments, inventory, shipping, notifications, warehouse); new ones must subscribe without changes to the order service.
- Consumers see an order's events in the order they happened.
Non-functional
- No event lost and no event for an order that does not exist.
- Checkout latency unaffected by consumers.
- 5 million orders per day, peak 10x average during sales.
C1.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Events per day | 5M orders x 6 events | 30M |
| Average rate | 30M / 86,400 | about 350 events/s |
| Peak rate | 10x average | about 3,500 events/s |
| Event size | JSON or Avro with envelope | about 1 KB |
| Peak throughput | 3,500 x 1 KB | about 3.5 MB/s |
| Storage for 30 days | 30 GB/day x 30 x RF 3 | about 2.7 TB before compression |
C1.3Topics and events
| Topic | Key | Partitions | Retention | Notes |
|---|---|---|---|---|
orders.events.v1 |
order_id |
24 | 30 days | All order event types, so per-order order holds |
<consumer>.orders.dlt |
order_id |
24 | 90 days | One DLT per consuming service |
- One topic for all order event types: consumers need
OrderCreatedbeforeOrderPaidfor the same order, and order exists only within one partition of one topic. - Partitions: 3,500 events/s at peak; if the slowest consumer handles about 500 events/s per instance, it needs at least 7. 24 leaves room to grow without remapping keys.
- Envelope:
event_id,event_type,event_time,order_id,schema_version, plus a body with what consumers need (items, amounts, address for shipping).
C1.4High-level design
orders.events.v1, keyed by order ID. Each downstream service consumes with its own group and applies events idempotently.- The order service writes the
ordersrow and anoutboxrow (event ID, type, key, payload) in one database transaction, then returns to the client. - Debezium reads the write-ahead log and, with its outbox event router, publishes each outbox row to
orders.events.v1with keyorder_id. - Each downstream service consumes with its own consumer group, applies events idempotently, and commits after processing.
C1.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1The dual-write problem
Why not write the order and then call
producer.send()?Write your answer, then reveal
A crash between the two leaves an order with no event (or, in the reverse order, an event with no order). Two systems cannot be updated atomically without help. The outbox makes the event part of the same database transaction; the relay then delivers it at least once.
- 2Relay choice
CDC with Debezium or a polling publisher?
Write your answer, then reveal
CDC reads the write-ahead log: low latency, no extra query load, strict commit order. A poller (
SELECT ... WHERE published = false ORDER BY id LIMIT 500 FOR UPDATE SKIP LOCKED, publish, mark published) is simpler to run without Connect but adds load and latency, and ordering across concurrent transactions needs care. Either way, delivery is at-least-once. - 3Ordering per order
Can a consumer see
OrderShippedbeforeOrderPaid?Write your answer, then reveal
Not if both are keyed by
order_idin one topic: they land in the same partition in commit order, and a consumer processes a partition in order. Ordering can still break on the consumer side (retry topics, worker pools); use blocking retries for ordered processing. - 4Idempotent consumers
The relay or a rebalance delivers
OrderPaidtwice. How does shipping avoid two shipments?Write your answer, then reveal
Shipping records processed
event_ids in its database, in the same transaction as creating the shipment, with a unique constraint. A duplicate is skipped. Business keys help too: "one shipment per order" as a unique constraint. - 5A new consumer next quarter
A loyalty service needs all orders from the last year. Retention is 30 days.
Write your answer, then reveal
Options: keep a compacted snapshot topic (
orders.state.v1, latest state per order) that the new service reads from the beginning; backfill from the warehouse; or use longer retention with tiered storage. The event stream gives changes; a snapshot gives current state cheaply. - 6Schema changes
Shipping needs
delivery_window. Inventory does not care.Write your answer, then reveal
Add
delivery_windowas an optional field with a default (BACKWARD compatible), register the schema through CI, deploy consumers that use it, then producers. Old consumers keep working. A breaking change goes toorders.events.v2with a migration period. - 7Outbox housekeeping
The outbox table grows by 30M rows a day.
Write your answer, then reveal
Delete published rows (Debezium only needs the insert in the log, so the router pattern can even delete the row in the same transaction), or partition the table by day and drop old partitions. Monitor replication slot lag so a stopped connector does not fill the database disk.
C1.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Debezium connector down | Events delayed; WAL accumulates in the database | Alert on slot lag; max_slot_wal_keep_size; restart quickly |
| Consumer bug | Bad side effects or DLT growth | DLT per consumer, alerts, replay tool after the fix |
| Hot key (one huge order) | Unlikely; orders are small | Not a concern with order_id keys |
| Broker failure | Brief leader failover | RF 3, min.insync.replicas=2, acks=all on the relay |
| Schema break | Consumers fail to deserialize | Registry compatibility checks in CI |
C1.7What a strong answer shows
SDE-2: names the dual-write problem and solves it with an outbox; keys by order_id for per-order order; idempotent consumers; separate consumer groups; schema registry. Senior adds: relay trade-offs (CDC vs polling), slot and outbox housekeeping, a compacted state topic for new consumers, consumer-side ordering pitfalls, and how choreography differs from an orchestrated saga for payment and inventory.
C1.8Follow-up questions
- Why not use a distributed transaction (two-phase commit) between the database and Kafka?
Reveal
Kafka does not take part in XA transactions with your database, and 2PC couples availability and adds latency. The outbox gives the same atomicity with one local transaction.
- How does the payments service tell the order service that payment succeeded?
Reveal
It publishes
PaymentSucceeded(through its own outbox) keyed byorder_id; the order service consumes it and moves the order to Paid. That is a choreographed saga; an orchestrator service is the alternative when steps grow. - How do you replay one day of events into a fixed consumer?
Reveal
Stop that consumer group, reset offsets with
--to-datetime, restart. Idempotent processing makes the replay safe. - What if the order service must also publish to two topics atomically?
Reveal
Two outbox rows in the same database transaction; the relay routes each to its topic. Or Kafka transactions, if the producer is a Kafka-to-Kafka processor.
Case study 2: Card transaction pipeline
A card issuer must turn every authorization into ledger postings, fraud signals, customer notifications, and an audit archive, then match the network's clearing records days later. Money moves on these events, so nothing may be lost or applied twice, and card data must stay protected.
Design the event pipeline behind card authorizations for an issuer processing 20 million transactions a day. Decide what stays synchronous, how events are published, topic and key design, how the ledger avoids double posting, how fraud features are computed in time windows, how clearing records are matched, how card data is protected, and how long data is kept.
Reveal the answer
A strong answer keeps the authorization decision synchronous and publishes its outcome through an outbox; keys everything by card token so a card's events stay ordered; uses acks=all, RF 3, min.insync.replicas=2; posts to the ledger idempotently by authorization ID; computes fraud features with event-time windows in Flink or Kafka Streams with exactly-once; matches clearing to authorization with a stateful join and an exception queue; never puts full card numbers in Kafka; archives to object storage for years; and plans replay and reconciliation.
C2.1Requirements
Functional
- For every authorization (approved or declined): post holds to the ledger, update fraud features, notify the customer, archive the event.
- Later, match clearing (settlement) records from the card network to authorizations, convert holds to postings, and raise exceptions for unmatched items.
- Support reversals and partial clearings.
Non-functional
- Authorization decisions within the network's deadline (tens to low hundreds of milliseconds); Kafka is not on that path.
- No lost and no duplicated ledger effects.
- Keep Kafka out of PCI DSS card data scope: no full card number (PAN) in events or logs.
- Audit retention for years.
C2.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Authorizations | 20M per day | about 230/s average |
| Peak | about 8x average (lunch, sales) | about 2,000/s |
| Events per authorization | decision, then posting and notification downstream | about 3 events across topics |
| Peak event rate | 2,000 x 3 | about 6,000/s |
| Event size | Avro with envelope | about 1.5 KB |
| Peak throughput | 6,000 x 1.5 KB | about 9 MB/s |
| Daily volume | 20M x 3 x 1.5 KB | about 90 GB/day |
| Hot retention | 90 GB x 7 days x RF 3 | about 1.9 TB before compression |
| Archive | 7 years in object storage | Sink connector, Parquet |
C2.3Topics and events
| Topic | Key | Partitions | Retention | Notes |
|---|---|---|---|---|
card.auth.events.v1 |
card token | 48 | 7 days | AuthorizationApproved, AuthorizationDeclined, AuthorizationReversed |
card.clearing.events.v1 |
card token | 48 | 30 days | Parsed from network clearing files |
ledger.postings.v1 |
account ID | 24 | 30 days | Output of the ledger service |
fraud.features.v1 |
card token | 48 | 3 days | Windowed features for scoring |
- Key by card token: a card's authorizations, reversals, and clearings stay in order on one partition, and the matcher can join auth and clearing streams because both are co-partitioned (same key, same partition count).
- Card token, not PAN: the tokenization service maps PAN to token; events carry the token and the last four digits only.
C2.4High-level design
card.auth.events, keyed by card token, and fans out to the ledger, fraud features, notifications, and the archive. Clearing records arrive later and are matched against authorizations by a stateful job.- The card network sends an authorization request; the auth service decides synchronously (balance, limits, rules, risk score) and writes the decision and an outbox row in its database.
- CDC publishes the outbox row to
card.auth.events.v1. - Consumers: the ledger service posts holds idempotently by
auth_id; a Flink job maintains fraud features; notifications send push or SMS; a sink connector archives to object storage. - Clearing files are parsed into
card.clearing.events.v1; a matcher joins them with authorizations by card token and authorization code within a time window, emits matched results, and routes breaks to an operations queue.
C2.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1What stays synchronous
Why not put Kafka in the authorization path?
Write your answer, then reveal
The network expects an answer within a strict deadline, and the decision needs current balance and limits. Adding an asynchronous hop adds latency and failure modes. The decision is synchronous against the source of truth; Kafka distributes the outcome to everything that can happen afterwards.
- 2Never lose, never double-post
Walk through what makes a ledger posting happen exactly once.
Write your answer, then reveal
Producer side: outbox in the same transaction as the decision, so every decision yields an event; relay with
acks=alland idempotence. Broker side: RF 3,min.insync.replicas=2, unclean election off. Consumer side: the ledger applies a posting and recordsauth_id(plus event type) in the same database transaction with a unique constraint, then commits the offset. Redeliveries hit the constraint and change nothing. - 3Ordering and reversals
A reversal arrives for an authorization the ledger has not seen yet. How?
Write your answer, then reveal
It should not happen within one partition, since both are keyed by card token and published in commit order. It can happen if the reversal comes from another system or a replay mixes sources. Make handlers robust: park a reversal for an unknown authorization briefly and retry, or record it and apply it when the authorization arrives (state machine per authorization).
- 4Fraud features in windows
Compute "number of transactions and total amount per card in the last 5 minutes" for scoring.
Write your answer, then reveal
A Flink or Kafka Streams job keyed by card token keeps windowed aggregates in event time (hopping or sliding windows), with a grace period for late events, and writes features to
fraud.features.v1and a low-latency store the risk engine reads. Exactly-once processing keeps counts correct across failures. The synchronous risk check reads the latest features; it does not wait for the stream. - 5Matching clearing to authorization
Clearing arrives 1 to 3 days later, sometimes for a different amount, sometimes never.
Write your answer, then reveal
A stateful job keyed by card token stores open authorizations (state store or database) and matches clearing records by authorization code, amount tolerance, and time window. Matches convert holds to postings; unmatched clearings and expired authorizations (holds released after N days) go to a reconciliation queue for operations. This is a stream-table style join with long-lived state, not a short windowed stream-stream join.
- 6Card data and access
How do you keep this PCI-friendly?
Write your answer, then reveal
No PAN in Kafka: tokens and last four digits only. TLS on all listeners, SASL or mTLS authentication, ACLs so only the ledger can read postings and only the auth service can write auth events, encryption at rest for brokers and archives, and audit of topic access. Keep cardholder data environments separate from analytics topics.
- 7Replay and audit
An auditor asks for all events of one card from 3 years ago; a bug requires reprocessing last Tuesday.
Write your answer, then reveal
Audit: the object storage archive (partitioned by date, queryable with a lake engine, with write-once retention such as S3 Object Lock so records cannot be altered), not Kafka. Never use a compacted topic as an audit trail: compaction removes the intermediate events an auditor needs. Reprocessing: retention of 7 days lets you reset the ledger consumer's offsets to Tuesday, and idempotency makes it safe; for anything older, replay from the archive into a dedicated topic.
C2.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Ledger database slow | Lag on postings; customers see stale balances | Batch writes per poll, alert on lag in time, scale to partition count |
| Broker loss | Short failover | RF 3 across zones, min.insync.replicas=2 |
| Relay down | Events delayed, not lost | Outbox persists; alert on outbox age |
| Late or missing clearing | Holds outstanding | Expiry rules, reconciliation queue |
| Hot card (merchant test card) | One partition busy | Rate limits upstream; per-card limits |
C2.7What a strong answer shows
SDE-2: synchronous decision plus asynchronous distribution; outbox; key by card token; acks=all with idempotence; idempotent ledger postings; windowed fraud features; no PAN in events. Senior adds: matching with long-lived state and reconciliation, exactly-once in the stream processor versus idempotency at the ledger, co-partitioning requirements, retention tiers (hot Kafka, archive), multi-region failover and its duplicate window.
C2.8Follow-up questions
- Could the ledger use Kafka transactions instead of a processed-IDs table?
Reveal
Only if its output were Kafka records. The ledger writes to a database, so Kafka transactions do not cover the effect; idempotency in the database transaction does.
- Why 48 partitions for
card.auth.eventsat about 2,000 events/s peak?Reveal
If one ledger instance posts about 100 events/s (one database transaction each), peak needs 20 instances; 48 leaves growth headroom, divides evenly by 2, 3, 4, 6, 8, 12, 16, and 24, and the clearing topic uses the same count so the two are co-partitioned.
- How would you notify customers without spamming duplicates during rebalances?
Reveal
Idempotency key per notification (
auth_id+ channel) stored before sending, and providers' idempotency features; accept that rare duplicates are possible after ambiguous timeouts. - What changes for multi-region?
Reveal
Run active-passive per region with MirrorMaker 2 or keep regional clusters with per-region ownership of cards. Ledger idempotency absorbs duplicates during failover; reconciliation catches gaps.
Case study 3: Notification fan-out with priorities
Many services need to reach users by SMS, email, and push. One-time passwords must arrive in seconds even while a marketing campaign sends 20 million messages, providers enforce rate limits, and nobody should get the same message twice.
Design a notification platform used by every team. Cover the request API, how preferences and templates are applied, topic layout, keeping urgent messages fast during a bulk campaign, provider rate limits, retries and failures, duplicates, and how you would measure delivery.
Reveal the answer
A strong answer separates traffic by channel and priority into different topics with their own workers, so bulk sends cannot delay urgent ones; uses a router to apply preferences, quiet hours, and templates; scales bulk workers with share groups (or consumer groups with enough partitions); enforces provider and per-user rate limits with token buckets; uses retry topics and a DLT for failures; deduplicates by notification_id before calling providers; and tracks delivery status from provider callbacks.
C3.1Requirements
Functional
- Any service sends a notification request: user, template, data, priority, and an idempotency key.
- Respect user preferences (opt-outs per channel and category), quiet hours, and frequency caps.
- Channels: SMS, email, push. Track status (sent, delivered, failed) for each message.
Non-functional
- Urgent messages (one-time passwords, fraud alerts) delivered to the provider within 5 seconds, p99, even during campaigns.
- 100 million notifications per day; a campaign can add 20 million in 30 minutes.
- No duplicates in normal operation; never exceed provider rate limits.
C3.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Average rate | 100M / 86,400 | about 1,200/s |
| Campaign burst | 20M / 1,800 s | about 11,000/s |
| SMS provider limit | contract | for example 1,000/s |
| Message size | request plus rendered content | about 2 KB |
| Peak throughput | 12,000 x 2 KB | about 24 MB/s |
| Backlog at SMS limit | 20M SMS at 1,000/s | about 5.5 hours (so campaigns must be paced or spread across providers) |
The last line is the important one: Kafka can absorb the burst, but the provider decides how fast it drains. Plan for backlog and make sure it never sits in front of urgent traffic.
C3.3Topics and events
| Topic | Key | Partitions | Consumers |
|---|---|---|---|
notify.requests.v1 |
user_id |
24 | Router (consumer group) |
notify.sms.high, notify.email.high, notify.push |
user_id |
12 each | Channel workers (consumer group) |
notify.sms.bulk, notify.email.bulk |
user_id |
12 each | Bulk workers (share group) |
notify.<channel>.retry-<delay> |
user_id |
6 each | Retry consumers |
notify.dlt |
user_id |
6 | Alerting and replay tool |
notify.status.v1 |
notification_id |
12 | Status service, analytics |
- Priority by topic: Kafka has no message priority. Separate topics with separate workers give urgent traffic its own lane.
- Key by
user_id: one user's messages stay in order on the router and high-priority lanes, and per-user frequency caps can be checked locally.
C3.4High-level design
- Services produce requests to
notify.requests.v1(or call a thin API that does). Each request carriesnotification_idas an idempotency key. - The router loads preferences and templates (cached), drops opted-out or capped messages, renders content, and produces to the right channel and priority topic.
- Channel workers take a rate-limit token, check the dedup store, call the provider, record the result, and publish status events.
- Failures that may succeed later go to retry topics; permanent failures and exhausted retries go to
notify.dlt. - Provider callbacks (delivered, bounced) update status through
notify.status.v1.
C3.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Keeping urgent messages fast
A 20 million message campaign starts. Why do one-time passwords still arrive in seconds?
Write your answer, then reveal
They never share a queue with the campaign.
notify.sms.highhas its own workers and its own share of the provider's rate (for example, reserve 20% of the SMS limit for high priority). The campaign backlog sits innotify.sms.bulkand drains at the remaining rate. Watch lag in time per topic, and alert on the high lane only if it exceeds a few seconds. - 2Share groups for bulk workers
Why use a share group for bulk channels?
Write your answer, then reveal
Bulk messages are independent and their order does not matter. A share group (production-ready since Kafka 4.2) hands out records, not partitions, so you can run more workers than partitions, acknowledge each record, and let Kafka count delivery attempts (
RELEASEto retry,REJECTfor permanent failure). On an older cluster, use a consumer group with enough partitions, which works but ties parallelism to the partition count. - 3Rate limits
The SMS provider allows 1,000 requests per second across all workers.
Write your answer, then reveal
Use a shared token bucket (for example in Redis) per provider and lane; a worker takes a token before each call. When tokens run out the worker waits, but must keep its loop healthy: a consumer group worker must still call
poll()withinmax.poll.interval.ms, and a share group worker must finish or renew records within the lock duration (30 s by default), so fetch small batches. Per-user caps ("at most 3 marketing SMS a day") are checked in the router. - 4Retries and failures
The provider returns a timeout, a 429, or "invalid phone number".
Write your answer, then reveal
Classify. Invalid number is permanent: send to the DLT (or mark failed) immediately. Timeouts and 5xx are transient: retry with backoff through
notify.sms.retry-30sandnotify.sms.retry-5m, then the DLT. A 429 means slow down: back off the whole worker, not just one message. For OTPs, a retry after a minute may be useless; give them a short time to live and drop expired ones. - 5Duplicates
A worker sends an SMS and crashes before its offset is committed. The record is delivered again.
Write your answer, then reveal
Before calling the provider, insert
notification_idinto a dedup store with a unique constraint (orSET NXwith a TTL in Redis); skip if present. Pass the same ID as the provider's idempotency key when supported. The remaining gap (crash after the provider accepted but before recording the result) cannot be closed fully; this is at-least-once with idempotency, and that is acceptable for notifications. - 6Measuring delivery
How do you know the system works?
Write your answer, then reveal
Per lane: request rate, lag in seconds, provider latency and error rate, token bucket saturation, DLT rate. Per message: status events from send through provider callback, so support can answer "did the user get it?". End-to-end: time from request to provider accepted, p50 and p99, for the high lanes.
C3.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Provider outage | Retries pile up; lane lag grows | Circuit breaker, failover to a second provider, pause bulk lanes |
| Router slow (preference store) | All channels delayed | Cache preferences, scale router to 24 partitions |
| Campaign floods SMS | Bulk backlog for hours | Separate lanes, reserved rate for urgent, pacing at campaign creation |
| Poison request (bad template data) | Repeated failures | Validate in router, send to DLT with the error |
| Duplicate sends | Annoyed users, extra cost | Dedup by notification_id, provider idempotency keys |
C3.7What a strong answer shows
SDE-2: priority through separate topics and workers, a router for preferences and templates, rate limiting, retry topics and a DLT, dedup by an idempotency key. Senior adds: rate reservation between lanes, why backlog is set by provider limits not Kafka, share groups versus partitions for parallelism, TTLs for time-sensitive messages, provider failover, and a status pipeline for support and analytics.
C3.8Follow-up questions
- Why not one topic with a priority field?
Reveal
A consumer reads a partition in order, so an urgent record waits behind every bulk record before it. Priority needs separate lanes.
- Should the router call providers directly?
Reveal
No. Splitting routing from sending lets each channel scale, fail, and rate-limit independently, and keeps a slow provider from blocking other channels.
- How would you schedule a notification for 9 AM in each user's time zone?
Reveal
Store scheduled notifications by due time and publish them when due (a scheduler, see the delayed jobs case study), rather than holding them in Kafka.
- Retry topics break ordering. Does that matter here?
Reveal
Rarely for notifications. Where it does (for example "card blocked" then "card unblocked"), keep those in the high lane with blocking retries, or include a version and drop stale messages.
Case study 4: Real-time ad click aggregation
Advertisers pay per click and watch dashboards in near real time. Count clicks per ad per minute from a billion daily events, correctly, despite duplicates, late arrivals, bots, and a few viral ads that get most of the traffic.
Design a system that counts ad clicks per ad per minute for dashboards (fresh within a minute) and billing (must be exact). Cover ingestion, partitioning, duplicates, event time and late data, hot ads, exactly-once, storage for queries, and how you would recompute after a bug.
Reveal the answer
A strong answer ingests clicks into Kafka keyed by click_id for even spread; deduplicates by click_id with a TTL store; filters bots; aggregates in event time with 1-minute tumbling windows and a grace period; handles hot ads with salted two-stage aggregation; runs the stream job with exactly-once; writes results keyed by (ad, window) to a compacted topic and an OLAP store with upserts; archives raw clicks; and reconciles billing from raw data, recomputing with a fresh application ID or batch job after bugs.
C4.1Requirements
Functional
- Count clicks per ad per minute; query by ad and time range; top ads in the last N minutes.
- Billing totals per advertiser per day must be exact.
Non-functional
- 1 billion clicks per day, peaks 5x average.
- Dashboards fresh within about a minute.
- Count each click once, even when clients retry and the pipeline restarts.
C4.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Average rate | 1B / 86,400 | about 11,600/s |
| Peak | 5x | about 60,000/s |
| Click event size | IDs, timestamps, user agent, IP | about 300 bytes |
| Peak ingest | 60,000 x 300 B | about 18 MB/s |
| Dedup state | 1 h of IDs at average rate: 42M x about 50 B | about 2 GB across instances (5x at peak) |
| Output | active ads x 1 row per minute | small, for example 500k rows/min |
C4.3Topics and events
| Topic | Key | Partitions | Retention | Notes |
|---|---|---|---|---|
ad.clicks.v1 |
click_id |
48 | 3 days | Raw clicks; also archived to object storage |
| internal repartition topics | ad_id + salt, then ad_id |
48 | Managed by Streams | Created by groupBy |
ad.counts.1m.v1 |
ad_id + window start |
24 | compacted | Latest count per ad per minute |
- Key raw clicks by
click_id: spreads load evenly and puts every duplicate of a click on the same partition, so deduplication is local. - Output keyed by (ad, window): writing a corrected count simply overwrites the old one, in the compacted topic and in the OLAP store.
C4.4High-level design
- Edge collectors receive clicks, attach a server timestamp and
click_id, and produce toad.clicks.v1(acks=all, idempotent, lz4). - A Kafka Streams or Flink job, with exactly-once processing:
- Filters bots (IP and device rate rules, deny lists).
- Deduplicates by
click_idusing a state store of IDs seen in the last hour. - Re-keys by
ad_idplus a salt (0 to 9) and pre-aggregates per 1-minute window. - Re-keys by
ad_idand sums the partial counts per window.
- Results go to
ad.counts.1m.v1, which an OLAP store (Pinot, Druid, or ClickHouse) ingests with upsert by key. - A sink archives raw clicks to object storage for billing reconciliation and recomputation.
C4.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Event time and late clicks
A mobile client buffers clicks offline and sends them 90 seconds late. Which minute do they count in?
Write your answer, then reveal
The minute they happened: aggregate by event time (the click's timestamp, validated against the server time), not arrival time. A grace period of 2 minutes keeps each window open for late records, then closes it. Clicks later than the grace period are dropped by the window (and counted in a dropped-records metric); billing picks them up in the daily reconciliation from raw data.
- 2Deduplication
Clients retry, so the same click arrives twice. How is it counted once?
Write your answer, then reveal
Every click gets a unique
click_idat the source. Because raw clicks are keyed byclick_id, duplicates land on the same partition and task, which keeps a store of IDs seen in the last hour and drops repeats. The store is backed by a changelog topic, so it survives restarts. Duplicates arriving more than an hour apart are rare and handled by reconciliation. - 3Hot ads
One viral ad gets 30% of all clicks. What happens if you key by
ad_id?Write your answer, then reveal
One partition and one task would take 30% of the load, and adding partitions would not help. Salt the key:
ad_idplus a random or hashed salt from 0 to 9 spreads the ad over ten partitions for the first aggregation. The second stage, keyed byad_idalone, only sums ten partial counts per window, which is cheap. - 4Exactly-once
The job crashes after updating state but before writing output. Do counts double?
Write your answer, then reveal
Not with exactly-once processing (
processing.guarantee=exactly_once_v2in Kafka Streams, or Flink checkpoints with a transactional Kafka sink). Input offsets, state changelog writes, and output records commit atomically, so a restart resumes from the last committed point. The OLAP store sits outside that transaction, which is why output is keyed by (ad, window) and loaded with upserts: a replayed result overwrites rather than adds. - 5Freshness versus final counts
Dashboards want updates every few seconds; billing wants the final number once.
Write your answer, then reveal
Dashboards can take intermediate updates as the window fills (with record caching to reduce churn). Billing needs final values: suppress output until the window closes (
suppress(untilWindowCloses)oremitStrategy(onWindowClose)in Kafka Streams), or compute billing from the closed windows plus daily reconciliation against raw clicks. - 6Recomputing after a bug
A filter bug overcounted for two days.
Write your answer, then reveal
Fix the code, then reprocess from Kafka (3 days retention) with a new application ID writing to a new output version, or run a batch job over the archived raw clicks. Because output keys are (ad, window), loading corrected rows overwrites the bad ones. Compare totals with the old output before switching readers.
C4.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Hot ad | One task overloaded | Salted two-stage aggregation |
| Stream task restart | Pause while state restores | Standby replicas, compacted changelogs |
| Clock skew on devices | Clicks in wrong minute or rejected | Use server receive time when device time is implausible |
| Bot traffic | Inflated counts | Filter before counting; reconcile billing |
| OLAP ingestion down | Dashboards stale, data safe | Kafka retains output; OLAP catches up |
C4.7What a strong answer shows
SDE-2: event time with tumbling windows and grace, dedup by click ID, exactly-once processing, hot key salting, keyed upserts into a query store. Senior adds: the boundary of exactly-once (Kafka to Kafka) and idempotent loading beyond it, final versus intermediate results, reconciliation for billing, reprocessing strategy, and state size and restore time.
C4.8Follow-up questions
- Why not key raw clicks by
ad_idfrom the start?Reveal
Hot ads would overload single partitions, and deduplication by
click_idwould still need all copies of a click together. Keying byclick_idgives even load and local dedup; re-keying later is a cheap repartition. - Could the OLAP store do all of this itself?
Reveal
Real-time OLAP stores can ingest raw clicks from Kafka and aggregate at query time or with rollups. That removes the stream job but makes deduplication, bot filtering, and exact billing harder. Many systems do both: OLAP on raw data for exploration, a stream job for trusted counts.
- How big should the grace period be?
Reveal
Look at the distribution of lateness (arrival time minus event time). A grace covering p99.9 keeps dropped clicks rare; longer grace delays final results and keeps more windows in state.
- What if the business wants hourly and daily counts too?
Reveal
Roll up from the minute counts (in the stream job or the OLAP store) rather than recounting raw clicks.
Case study 5: Centralized log pipeline
Thousands of pods write logs. Engineers need to search the last week in seconds, compliance needs a year of history, and a logging spike from one buggy service must not take down search for everyone else.
Design a pipeline that collects logs from 5,000 pods and makes them searchable within seconds, keeps a year of history cheaply, and alerts on error spikes. Cover collection, topic layout, keys, compression, retention, sinks, protecting the cluster from noisy services, sensitive data, and what happens when search is slow.
Reveal the answer
A strong answer uses node agents with disk buffers that produce to Kafka with no key, lz4 or zstd compression, and large batches; splits topics by team or tier for retention, ACLs, and quotas; keeps a few days in Kafka as a buffer; uses Connect sinks to a search cluster (short retention) and object storage (long retention) with idempotent writes; adds a small stream job for error-rate alerts; scrubs secrets and personal data at the agent; and relies on Kafka to absorb spikes and sink outages.
C5.1Requirements
Functional
- Collect application logs from all pods; search the last 7 days by service, time, level, and text.
- Keep all logs for 1 year at low cost; alert when a service's error rate spikes.
Non-functional
- Searchable within about 30 seconds of being written.
- Survive a search cluster outage of several hours without losing logs.
- Losing a few log lines in rare failures is acceptable; losing hours is not.
C5.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Lines per second | 5,000 pods x 40 lines/s | 200,000/s |
| Raw volume | 200,000 x 400 B | about 80 MB/s, about 7 TB/day |
| On the wire with lz4 | text compresses about 4x | about 20 MB/s |
| Kafka storage | 7 TB / 4 x 3 days x RF 3 | about 16 TB |
| Search (7 days hot) | 7 TB x 7, plus index overhead | about 50 TB or more |
| Archive (1 year) | Parquet compresses about 10x | about 250 TB in object storage |
C5.3Topics and events
| Topic | Key | Partitions | Retention | Notes |
|---|---|---|---|---|
logs.<team> |
none | 6 to 24, by volume | 3 days | One per team: own quota, ACLs, retention |
logs.audit |
none | 12 | 7 days | acks=all, min.insync.replicas=2; stricter handling |
logs.alerts |
service name | 6 | 1 day | Output of the alerting job |
- No key: log lines need no per-key order (search sorts by timestamp), so keyless records with the sticky partitioner build full batches and spread load evenly.
- Per-team topics: one noisy team cannot change another team's retention, and quotas and ACLs map cleanly to teams.
C5.4High-level design
- An agent on each node (Fluent Bit or Vector) tails container logs, parses them to JSON, removes secrets and personal data, and produces to the team's topic with large batches (
linger.ms50 to 100,batch.size256 KB or more) and lz4 compression. It buffers on local disk if Kafka is unreachable. - Kafka keeps 3 days, so a sink can be down for a long weekend without losing data.
- Connect sinks write to OpenSearch (7 days) and to object storage as Parquet partitioned by date and team (1 year).
- A small stream job counts errors per service per minute and publishes alerts.
- Broker quotas cap each team's produce rate.
C5.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Why put Kafka in the middle?
Agents could write straight to the search cluster. Why not?
Write your answer, then reveal
Search clusters ingest slowly under spikes and go down for upgrades. Kafka absorbs bursts, keeps days of buffer, and lets several consumers (search, archive, alerting, security tools) read the same data independently. Without it, every agent needs retry logic for every destination, and a slow destination backs up into the pods.
- 2Throughput settings
What makes the producers efficient at 200,000 lines per second?
Write your answer, then reveal
Big batches and compression:
linger.msaround 50 to 100 and a largebatch.sizelet each request carry hundreds of KB, and lz4 (or zstd for better ratio) compresses whole batches about 4x. Keyless records use the sticky partitioner, which fills one batch at a time.acks=allwith idempotence costs little at this batch size; for debug logs,acks=1is an acceptable trade if you want lower latency. - 3Noisy neighbors
A buggy service logs 50 times its normal volume.
Write your answer, then reveal
Per-team produce quotas (
producer_byte_rateper user or client ID) make the broker throttle that team instead of filling disks and slowing everyone. The agent's disk buffer absorbs the throttling, and an alert tells the team. Per-team topics also confine the extra retention cost. - 4Sink outage
OpenSearch is down for 6 hours.
Write your answer, then reveal
The sink connector's lag grows; Kafka still has the data (3 days). When search returns, the connector catches up, faster if you add tasks up to the partition count. Writes are idempotent if the document ID comes from topic, partition, and offset, so retries after the outage overwrite instead of duplicating.
- 5Sensitive data
A service logs full request bodies with card numbers and tokens.
Write your answer, then reveal
Scrub at the agent, before data leaves the node: drop known secret fields and mask patterns such as card numbers and emails. Doing it later means the data already sits in Kafka, backups, and every consumer. Add ACLs so only the sinks and the security team read raw logs, and keep
logs.auditseparate. - 6Alerting on error spikes
How do you page on a service whose error rate jumps?
Write your answer, then reveal
A Kafka Streams or Flink job filters
level=ERROR, counts per service in 1-minute windows, compares with a baseline, and publishes tologs.alerts, which an alert manager consumes. Small state and simple logic; it does not need exactly-once, since an occasional double count does not change an alert.
C5.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Search cluster slow or down | Search lag grows | Kafka buffer (3 days), catch up with more tasks |
| Log storm from one service | Broker load, disk use | Quotas, per-team topics, agent sampling of debug logs |
| Broker failure | Brief failover | RF 3; keyless records route around unavailable partitions |
| Agent cannot reach Kafka | Logs pile up on nodes | Disk buffer with a size cap; drop oldest debug logs first |
| Malformed lines | Indexing errors | Connect errors.tolerance=all with a DLQ topic |
C5.7What a strong answer shows
Starter level: Kafka as a buffer between many producers and slow sinks, keyless records with batching and compression, retention as a safety margin, Connect sinks for search and archive. SDE-2 adds: quotas and per-team topics, idempotent sink writes, scrubbing at the source, separate handling for audit logs, and capacity numbers for each layer.
C5.8Follow-up questions
- Would you use the same pipeline for metrics?
Reveal
Usually not. Metrics are small, numeric, and queried by time series databases; Prometheus-style scraping or remote write is simpler. Kafka helps for metrics only at very large scale or when many consumers need the raw stream.
- Why only 3 days in Kafka if search keeps 7?
Reveal
Kafka is the buffer, not the store. 3 days covers sink outages and replays; search and object storage hold the long-term copies more cheaply.
- How would you reduce cost?
Reveal
Drop or sample debug logs at the agent, shorten hot search retention, use zstd, and query older logs from object storage with a lake engine instead of keeping them in search.
- Should you key by pod to keep each pod's lines in order?
Reveal
Usually no. Timestamps order lines at query time, and pod keys make busy pods hot partitions. Key by pod only if a consumer truly needs per-pod sequence.
Case study 6: Change data capture to search and cache
A product catalog lives in PostgreSQL. Search, the cache, and a product view API must reflect every change within seconds, without the application writing to four systems, and the search index must be rebuildable at any time.
Keep a search index, a Redis cache, and a denormalized product view in sync with a PostgreSQL catalog. Cover how changes are captured, topic configuration, deletes, ordering and stale writes, rebuilding the index from scratch, joining two tables, schema changes, and operating the database side.
Reveal the answer
A strong answer uses Debezium to stream row changes into compacted topics keyed by primary key; has independent consumers for search (upsert, delete on delete events) and cache (evict or versioned set); uses source versions (LSN) to reject stale writes; rebuilds search by replaying the compacted topic into a new index and swapping an alias; joins products and stock with a table-table join; handles schema evolution through the registry; and watches replication slot lag. It also notes that raw CDC couples consumers to the table schema, and uses an outbox for events meant for other teams.
C6.1Requirements
Functional
- Search reflects product changes (name, price, description, availability) within seconds.
- The cache never serves data older than a few seconds after a change.
- A product view combining product and stock is available to APIs.
- Rebuild the search index from scratch without downtime.
Non-functional
- 50 million products; 2,000 changes per second on average, 5,000 at peak (mostly stock).
- No dual writes in application code.
C6.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Change rate | stock and price updates | 2,000/s average, 5,000/s peak |
| Change event size | Debezium envelope with before and after | about 2 KB |
| Peak throughput | 5,000 x 2 KB | about 10 MB/s |
| Compacted products topic | 50M x about 2 KB | about 100 GB per replica |
| Full reindex | 50M documents at 20,000/s | about 45 minutes |
C6.3Topics and events
| Topic | Key | Partitions | Cleanup | Notes |
|---|---|---|---|---|
shop.public.products |
id |
24 | compact | Debezium names topics <prefix>.<schema>.<table> |
shop.public.stock |
product_id |
24 | compact | Same partition count as products, for the join |
product_view.v1 |
product_id |
24 | compact | Output of the join, read by APIs |
- Compacted: the topic always holds the latest change for every row, so a new consumer can rebuild full state by reading from the beginning. Deletes become tombstones that compaction eventually removes.
- Keyed by primary key: all changes to one row are in one partition, in commit order.
C6.4High-level design
- Debezium (on Kafka Connect) takes an initial snapshot of the tables, then reads the write-ahead log through a replication slot and publishes each committed row change.
- The search indexer consumes
products, upserts documents byid, and deletes documents on delete events. - The cache updater evicts (or sets) Redis keys per change.
- A Kafka Streams job joins products and stock as tables and writes
product_view.v1, which APIs read or which feeds another store.
C6.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Why CDC instead of application writes?
The service could update the database, then Elasticsearch, then Redis.
Write your answer, then reveal
That is the dual-write problem three times: a crash or failure between writes leaves systems inconsistent, and every service that writes products must remember all of it. CDC reads the database's own log, so every committed change is captured once, in commit order, and nothing in the application changes when you add a consumer.
- 2Deletes
A product is deleted. How does it leave search and the cache?
Write your answer, then reveal
Debezium emits a delete event (with the old row in
before) and, by default, a tombstone (same key, null value). Consumers treat either as "remove this key": delete the search document, evict the cache key. In the compacted topic the tombstone stays fordelete.retention.ms(1 day by default) so consumers have time to see it, then disappears. - 3Stale writes
During a replay or a retry, an older version of a product reaches the index after a newer one.
Write your answer, then reveal
Within a partition, order is preserved, but replays and parallel workers can still apply old data late. Use the change's source position (the PostgreSQL LSN, or an
updated_atversion column) as an external version: the index rejects writes with a lower version than it holds (OpenSearch and Elasticsearch supportversion_type=external). For Redis, evicting instead of setting avoids the problem entirely; reads repopulate from the database. - 4Rebuilding search
A new analyzer requires a full reindex, with no downtime.
Write your answer, then reveal
Create
products_v2, start a new consumer group onshop.public.productsfrom the earliest offset, and index everything. Because the topic is compacted, this is the full current catalog plus recent changes, not years of history. When the new group's lag reaches near zero, point theproductsalias atproducts_v2, then stop the old indexer and drop the old index. - 5Joining products and stock
APIs want price and availability in one document.
Write your answer, then reveal
In Kafka Streams, read both topics as
KTables and join on the product ID; both topics have the same key and partition count, so the join is local per partition. Each change to either side emits an updated view. If the key differed (stock keyed by warehouse and SKU), you would re-key first or use a foreign-key table join. - 6Schema changes
A developer adds a column, another renames one.
Write your answer, then reveal
Debezium registers the new value schema; with Avro and a registry, adding a nullable column is backward compatible and consumers carry on. A rename is a drop plus an add, which breaks consumers expecting the old field. Raw CDC exposes the table design to every consumer, so keep it for internal read models and publish curated events through an outbox for other teams.
- 7The database side
The connector stops for a weekend.
Write your answer, then reveal
The replication slot keeps WAL until Debezium confirms it, so the database disk fills. Alert on slot lag, set
max_slot_wal_keep_sizeas a safety limit (accepting a re-snapshot if exceeded), and configure heartbeats (heartbeat.interval.ms) so the slot advances even when the captured tables are quiet.
C6.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Connector down | Search and cache go stale; WAL grows | Slot lag alerts, quick restart, WAL size cap |
| Search cluster slow | Indexer lag; search is stale | Bulk requests, more partitions and tasks, lag alerts in seconds |
| Stale write after replay | Older data in index | External versioning by LSN |
| Bulk update of 10M rows | Burst of changes | Kafka buffers it; indexer catches up; schedule big updates off-peak |
| Breaking schema change | Consumers fail | Registry compatibility checks; outbox for public events |
C6.7What a strong answer shows
SDE-2: CDC instead of dual writes, compacted topics keyed by primary key, handling deletes and tombstones, idempotent upserts, rebuilding from the compacted topic with an alias swap. Senior adds: version-based rejection of stale writes, eviction versus setting for caches, co-partitioning for joins, slot and WAL operations, and when to prefer an outbox over raw CDC.
C6.8Follow-up questions
- Why is reading a compacted topic from the beginning enough to rebuild the index?
Reveal
Compaction keeps at least the latest record for every key, so the topic contains the current state of every row (plus recent history), and deleted rows have been removed or appear as tombstones.
- Can the cache ever serve stale data?
Reveal
Briefly: between the database commit and the eviction (CDC lag, usually under a second), and when a read repopulates the cache just before an eviction arrives. Short TTLs bound the worst case.
- How do you add a new consumer that needs only the last week of changes?
Reveal
Start its group with
auto.offset.resetset toby_duration:P7D, or reset offsets with--to-datetime. On a compacted topic, older updates may already have been compacted away. - Why not query the database directly instead of keeping a read model?
Reveal
For simple reads, do. Read models help when queries need denormalized data, full-text search, or load the primary database cannot take.
Case study 7: Design a distributed log (Kafka itself)
The interviewer says "design Kafka". This tests whether you understand why Kafka works the way it does: partitioned append-only logs, sequential disk I/O, pull-based consumers, leader-follower replication with an in-sync set, and a separate metadata quorum.
Design a durable, horizontally scalable, replayable message log. Cover the data model, the storage format on disk, how writes are replicated and acknowledged, failure handling and leader election, where metadata lives, the producer and consumer protocols, and how the system scales.
Reveal the answer
A strong answer partitions topics and orders only within a partition; stores each partition as segment files with sparse offset and time indexes; relies on sequential writes, the page cache, batching, and zero-copy reads; replicates by followers pulling from a leader, tracking an in-sync replica set and a high watermark; uses leader epochs to truncate divergent logs after failover; keeps metadata in a small Raft quorum; makes producers idempotent with producer IDs and sequence numbers; lets consumers pull and store their offsets in an internal compacted log; and scales by adding partitions and brokers, with tiered storage for long retention.
C7.1Requirements
Functional
- Producers append records to named topics; consumers read from any position and replay.
- Ordering per key; many independent consumer groups; records kept by time or size.
Non-functional
- 1 GB/s writes, 3 GB/s reads; 3 days retention.
- No acknowledged write lost when one machine fails; failover within seconds.
- Horizontal scale by adding machines.
C7.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Disk writes with RF 3 | 1 GB/s x 3 | 3 GB/s across the cluster |
| Network in | producers 1 GB/s + replication 2 GB/s | 3 GB/s |
| Network out | consumers 3 GB/s + replication 2 GB/s | 5 GB/s |
| Storage | 1 GB/s x 86,400 x 3 days x RF 3 | about 780 TB |
| Brokers for storage | 780 TB / 12 TB usable each | about 65 |
| Brokers for throughput | about 300 MB/s in and 500 MB/s out each | about 10 to 15 |
Storage, not throughput, sets the broker count here. Tiered storage (old segments in object storage) would let 15 to 20 brokers do the job.
C7.3High-level design
- Topics and partitions: a topic is split into partitions; each partition is an ordered, append-only log on one leader broker with followers on other brokers. Keys hash to partitions.
- Brokers: store partitions, serve produce and fetch requests, replicate from leaders.
- Controller quorum: 3 or 5 nodes running Raft; holds the metadata log (topics, partitions, leaders, ISRs, configs) and elects partition leaders.
- Clients: producers batch by partition and send to leaders; consumers pull from leaders (or nearby followers) and commit offsets.
C7.4Deep dives
Guided designDeep divesAnswer each before revealing
- 1Storage on disk
How is a partition stored so that writes and reads are fast?
Write your answer, then reveal
A directory of segment files. Writes append to the active segment, which is sequential I/O that disks and the page cache handle very well. Each segment has a sparse offset index (an entry every 4 KB of data) and a time index, so finding an offset is a binary search plus a short scan. Retention deletes whole old segments, which is cheap. Records are stored in the same batch format producers send, so brokers do not re-encode them, and reads use
sendfileto copy from page cache to socket without passing through the application (zero-copy, when TLS is not used). - 2Replication and acknowledgement
When is a write safe to acknowledge?
Write your answer, then reveal
Followers fetch from the leader like consumers. The leader tracks which replicas are caught up (the in-sync replica set, ISR; a replica falls out after
replica.lag.time.max.ms). The high watermark is the highest offset every ISR member has; consumers only see records below it. Withacks=all, the leader answers once all current ISR members have the record, and refuses writes if the ISR is smaller thanmin.insync.replicas. With RF 3 and min ISR 2, any single failure loses no acknowledged write. - 3Why an ISR and not a majority quorum per write?
Raft would need a majority. What is the trade-off?
Write your answer, then reveal
To survive f failures, a majority quorum needs 2f + 1 replicas; an ISR needs f + 1 (every acknowledged write is on every in-sync replica, so any of them can lead). The cost: a write waits for the slowest in-sync replica until it is removed from the ISR, while a majority quorum ignores the slowest. Kafka uses ISR for data and Raft for metadata; some Kafka-compatible systems use Raft per partition instead.
- 4Failover and divergent logs
The leader dies after appending records that followers had not copied.
Write your answer, then reveal
The controller notices missed heartbeats (
broker.session.timeout.ms, 9 s by default) and picks a new leader from the ISR, bumping the leader epoch. The old leader's unreplicated tail was never below the high watermark, so it was never acknowledged withacks=all. When it returns, it asks the new leader where its epoch ended and truncates everything after that point before following. Electing a replica outside the ISR (unclean election) would lose acknowledged data, so it is off by default. - 5Metadata
Where do topic definitions, leaders, and ISRs live?
Write your answer, then reveal
In a replicated metadata log managed by a Raft quorum of controllers (KRaft). The active controller makes decisions and appends them; brokers replicate the log and keep metadata in memory, so any broker can tell clients who leads each partition. Keeping metadata separate and small makes failover fast even with hundreds of thousands of partitions.
- 6Producers
How do producers achieve high throughput without duplicates on retry?
Write your answer, then reveal
They batch records per partition (
batch.size,linger.ms), compress whole batches, and pipeline several requests per connection. For exactly-once appends, each producer gets a producer ID and numbers its batches per partition; the leader remembers the last sequence numbers and drops duplicates from retries, while still allowing up to 5 requests in flight with ordering preserved. - 7Consumers
Why pull instead of push, and where do offsets live?
Write your answer, then reveal
Pull lets each consumer go at its own pace, batch naturally, and rewind to replay; the broker stays simple and does not track per-message state. Long polling (
fetch.min.bytes,fetch.max.wait.ms) avoids busy loops. A group coordinator assigns partitions to group members, and committed offsets are records in an internal compacted topic keyed by (group, topic, partition), so the latest commit per partition survives. - 8Scaling and operations
Traffic doubles. What do you do?
Write your answer, then reveal
Add brokers, then reassign some partition replicas onto them (throttled copying); add partitions to topics that need more parallelism, knowing that keys will map differently. Spread replicas across racks or zones. For long retention, move old segments to object storage so local disks hold only recent data.
C7.5Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Broker crash | Leaders on it move to ISR followers | RF 3, min ISR 2, rack-aware placement |
| Slow follower | Writes wait until it leaves the ISR | replica.lag.time.max.ms, monitoring |
| Controller failure | A standby controller takes over | 3 or 5 controller quorum |
| Disk full | Partitions go offline on that broker | Retention by size and time, alerts, tiered storage |
| Too many partitions | Slower failover, more memory | Size partitions by throughput, not by habit |
C7.6What a strong answer shows
SDE-2: partitioned append-only logs, segments and indexes, page cache and sequential I/O, leader-follower replication with ISR and high watermark, acks and min ISR, consumer offsets as data. Senior adds: ISR versus majority quorum, leader epochs and truncation, separate metadata quorum, idempotent producer sequence numbers, zero-copy and its limits with TLS, and storage-driven sizing with tiered storage.
C7.7Follow-up questions
- Why does Kafka not fsync every write?
Reveal
Durability comes from replication: a write acknowledged with
acks=allis in memory on several machines, which is unlikely to fail together. Flushing is left to the operating system for throughput; forcing fsync is possible but rarely worth it. - How would you add exactly-once processing across partitions?
Reveal
Transactions: a coordinator writes transaction state to an internal log, and markers written to every involved partition commit or abort the records together; consumers with
read_committedskip aborted ones. - How would you support millions of topics?
Reveal
Partitions carry fixed costs (files, memory, metadata), so millions of tiny topics strain the design. Group them into fewer topics with a tenant field, or use a system built around many lightweight topics.
- Why is ordering only per partition?
Reveal
Total order would need one writer path for the whole topic, which caps throughput at one machine. Per-partition order scales, and keys give order where it matters.
Case study 8: Delayed and scheduled jobs
Kafka delivers records as soon as they are written; it has no per-message delay. Yet teams need "retry in 10 minutes", "send a reminder in 3 days", and "charge the subscription on the 1st". This case shows the two patterns that work and why a single delay topic does not.
Design a service that runs jobs at a requested time, from seconds to months ahead: reminders, subscription renewals, retries. 10 million jobs are scheduled per day, jobs can be cancelled or rescheduled, and each must run once, within a few seconds of its due time. Where does Kafka fit, and where does it not?
Reveal the answer
A strong answer explains head-of-line blocking (one topic cannot hold arbitrary delays); uses delay tier topics for a few fixed delays, where each tier is ordered by due time and a consumer pauses until the head is due; uses a store indexed by due time plus pollers (FOR UPDATE SKIP LOCKED or time buckets) for arbitrary times; publishes due jobs to an execution topic consumed by idempotent workers (a share group fits); handles cancellation with a status check at run time; and plans for spikes at popular times such as midnight or the top of the hour.
C8.1Requirements
Functional
- Schedule a job (type, payload, due time), cancel it, or reschedule it.
- Recurring jobs (every month on the 1st).
- Query job status.
Non-functional
- 10 million new jobs per day; up to 100 million pending.
- Run within about 5 seconds of the due time.
- Each job runs at least once and has effect once (idempotent handlers).
C8.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| New jobs | 10M / 86,400 | about 115/s |
| Pending jobs | up to 100M x about 1 KB | about 100 GB in the store |
| Due-time spike | 1M renewals due at 00:00 on the 1st | 1M jobs to dispatch at once |
| Dispatch rate needed | 1M in about 2 minutes | about 8,000/s |
The spike matters more than the average: popular times cluster. Either the business accepts spreading (jitter within a window), or dispatch and workers must handle the burst.
C8.3Topics and events
| Topic | Key | Partitions | Notes |
|---|---|---|---|
delay.1m, delay.10m, delay.1h |
original key | 6 to 12 each | Pattern A: fixed delay tiers |
jobs.execute |
job_id |
24 | Due jobs to run, from either pattern |
jobs.results |
job_id |
12 | Outcomes for status and auditing |
C8.4High-level design
Why not one delay topic? A consumer reads a partition in order. If a record due next month sits ahead of one due in a minute, the second waits a month. This is head-of-line blocking, and it rules out arbitrary delays in a single topic.
Pattern A, delay tiers: every record in delay.10m has the same delay, so records are already ordered by due time (write time plus 10 minutes). The tier consumer looks at the head record: if it is due, it forwards it to jobs.execute (or back to the original topic); if not, it pauses the partition until it is due, still calling poll(). This is how retry topics work (Spring Kafka's @RetryableTopic does the same).
Pattern B, scheduler with a store: the API writes jobs to a database indexed by due_at. Pollers repeatedly claim due jobs and publish them to jobs.execute. Workers run them and report results.
C8.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1When to use which
Retries after 1, 10, and 60 minutes versus reminders at arbitrary times.
Write your answer, then reveal
A few fixed delays: tiers. Simple, all in Kafka, no database, ordered and cheap. Arbitrary times, long delays, cancellation, and queries: a store. Holding a job in Kafka for weeks means long retention and no way to cancel or query it. Many systems use both: tiers for retries, a scheduler for business schedules.
- 2Claiming due jobs safely
Several poller instances run for availability. How do they avoid sending the same job twice?
Write your answer, then reveal
Each poller runs
SELECT ... WHERE due_at <= now() AND status = 'scheduled' ORDER BY due_at LIMIT 500 FOR UPDATE SKIP LOCKED, publishes the batch, marks the rowsenqueued, and commits.SKIP LOCKEDlets pollers share work without blocking each other. A crash after publishing but before commit sends those jobs again later, so workers must be idempotent byjob_id. To remove the gap, mark the rows and insert outbox rows in one transaction and let CDC publish (the outbox pattern). - 3Scaling the store
100 million pending jobs and 8,000 dispatches per second at midnight.
Write your answer, then reveal
Partition the jobs table by due time (for example by day), so the pollers touch only the current partition, and old partitions are dropped. Shard further by
hash(job_id)with one poller per shard if one database is not enough. A key-value store keyed by (minute bucket, shard) works the same way: a poller reads the current bucket for its shard. - 4Execution
Who runs the job, and what if it fails?
Write your answer, then reveal
Workers consume
jobs.execute. Jobs are independent, so a share group fits well: more workers than partitions, per-record acknowledgement, and a delivery attempt limit. A consumer group also works. Workers check that the job is still active, run it idempotently, and publish tojobs.results. Failures retry with delay tiers, then go to a DLT. - 5Cancel and reschedule
A user cancels a reminder 1 second before it is due.
Write your answer, then reveal
Mark the row
cancelled(or updatedue_atto reschedule, with a version number). If the job was already published, the worker re-reads its status before running and skips it if cancelled or if the version does not match. Some jobs will be too late to cancel; the API reports that honestly. - 6Recurring jobs
Charge every subscription on the 1st of each month.
Write your answer, then reveal
Store the schedule rule; when a run is dispatched (or completes), compute the next due time and insert the next job in the same transaction. Each run gets its own
job_id(for examplesubscription_idplus period), which also serves as the idempotency key for the charge. - 7Precision and time
How close to the due time does a job run?
Write your answer, then reveal
Poll interval (about a second) plus publish and consume latency, usually under a few seconds. Use database time (or a single trusted clock) for
now(), not each poller's local clock. Monitor dispatch delay (now minusdue_atat publish) and worker lag in seconds.
C8.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Midnight spike | Dispatch and workers saturated | Jitter where allowed, pre-scaled workers, batch claims |
| Poller crash | Jobs claimed but not committed are retried | Transaction rollback; idempotent workers |
| Worker crash mid-job | Job redelivered | Idempotency by job_id; share group delivery limit |
| Database slow | Dispatch delay grows | Index on (status, due_at), partitioning, sharding |
| Long-delayed records in a tier | Tier lag looks huge | Measure lag as time behind due, not record count |
C8.7What a strong answer shows
SDE-2: knows Kafka has no per-message delay and why one delay topic fails, delay tiers with pause until due, a store plus pollers with SKIP LOCKED, idempotent workers, cancellation through a status check. Senior adds: spikes at popular times, partitioning and sharding the store, outbox to close the publish gap, recurring jobs with per-run IDs, clock discipline, and meaningful lag metrics for delayed topics.
C8.8Follow-up questions
- Could Kafka Streams act as the scheduler?
Reveal
Yes for some cases: keep pending jobs in a state store keyed by due time and use a wall-clock punctuator to emit due ones every second. It avoids a database, but cancellation, queries, and very long delays are harder than with a store.
- Why does the tier consumer pause instead of sleeping?
Reveal
Sleeping stops
poll(), and aftermax.poll.interval.msthe consumer is removed from the group. Pausing keeps polling (and the group membership) while fetching nothing from that partition. - How do you handle a job that must run exactly at 09:00:00?
Reveal
You cannot guarantee exact timing in a distributed system. Dispatch slightly early and let the worker wait until the exact time, and design the business process to tolerate seconds of delay.
- Can share groups delay redelivery?
Reveal
Not in Kafka 4.3: a released or timed-out record is offered again without a configurable delay (up to the delivery count limit), so share groups do not replace delay tiers or a scheduler.
Case study 9: Multi-region disaster recovery
A payments platform runs in one region across three zones. The business now wants it to keep working if the whole region goes down, with at most a minute of lost events and back in service within 15 minutes. Design the Kafka side and the runbook.
Extend an existing single-region Kafka platform (order and payment events, about 20,000 events/s) so it survives a full region outage. Cover the topology choice, how data and consumer positions reach the second region, what producers and consumers do during failover, duplicates and gaps, the databases behind the services, failing back, and how you prove it works.
Reveal the answer
A strong answer first separates zone failures (already handled by RF 3 across zones) from region failures; picks active-passive with MirrorMaker 2 running in the backup region, explaining why a stretch cluster costs too much latency; replicates topics, configs, ACLs, schemas, and consumer offsets; states the RPO as replication lag and keeps it visible; makes consumers idempotent because translated offsets cause duplicates; treats databases as the systems of record with their own replication and reconciliation; writes failover and failback runbooks with fencing; and rehearses them on a schedule.
C9.1Requirements
Functional
- Keep accepting and processing order and payment events if the primary region is lost.
- Resume consumers in the backup region close to where they stopped.
- Return to the primary region after recovery without losing the writes made during the outage.
Non-functional
- RPO (data loss on region loss): at most 60 seconds of events.
- RTO (time to recover): 15 minutes.
- Normal write latency unchanged (local, a few milliseconds).
- No double charges after failover.
C9.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Event rate | given | about 20,000/s peak |
| Event size | Avro | about 2 KB |
| Cross-region replication | 20,000 x 2 KB, compressed about 3x | about 13 MB/s, about 1.1 TB/day of transfer |
| Replication lag budget | RPO target | well under 60 s; alert at 10 s |
| Checkpoint interval | MirrorMaker 2 default | 60 s (shorter for critical groups) |
| Inter-region round trip | typical | 20 to 60 ms |
The round-trip time rules out synchronous replication for every write: a stretch cluster would add it to each acks=all produce.
C9.3Topology
| Option | RPO | Write latency | Verdict |
|---|---|---|---|
| Single cluster across zones (today) | Region loss: everything | Local | Keep for zone failures |
| Stretch cluster across 3 regions | 0 | Plus a cross-region round trip per write | Too slow and costly for 20,000/s; needs a third region for the controller majority |
| Active-passive with MirrorMaker 2 | Replication lag (seconds) | Local | Chosen |
| Active-active with MirrorMaker 2 | Seconds for the remote region's data | Local | Only if users must write in both regions; complicates every consumer |
C9.4High-level design
- Producers and consumers run in the primary region against the primary cluster (RF 3 across zones, as today).
- MirrorMaker 2 runs in the backup region ("consume from remote, produce to local"), copying topics into prefixed remote topics (
primary.orders) and syncing topic configs and ACLs. - Checkpoints translate consumer group offsets; with
sync.group.offsets.enabledthe translated offsets are written into the backup cluster's__consumer_offsetsfor groups not running there. - The schema registry and the service databases replicate to the backup region by their own mechanisms.
- Standby consumers and producers are deployed in the backup region but idle (or scaled to zero) until failover.
C9.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Why not a stretch cluster?
Zero data loss sounds better. Why not one cluster across three regions?
Write your answer, then reveal
Every
acks=allwrite would wait for in-sync replicas in other regions, adding tens of milliseconds to every produce, and every replica copy would cross a region boundary, which is the most expensive network path. It also needs a third region for the KRaft controller majority. It fits a small, critical stream where zero loss beats latency; for the whole platform, asynchronous replication with a seconds-long RPO is the usual trade. - 2What exactly is replicated?
List everything the backup region needs before a failover can work.
Write your answer, then reveal
Topic data (MirrorMaker 2 source connector), topic configs and ACLs (MirrorMaker 2 sync options), consumer group offsets (checkpoints and group offset sync), schemas with the same IDs (the registry must be replicated too, or deserialization fails), connector configs, client credentials and certificates, and the databases the services own. Missing any one of these is the classic surprise during a real failover.
- 3Running the failover
The primary region is unreachable. Walk through the first 15 minutes.
Write your answer, then reveal
Declare the incident and fence the old region (stop its producers and relays if any part is still alive, so two regions never accept writes for the same data). Switch producers to the backup cluster through DNS or configuration; they write to local topics such as
orders. Start consumers subscribed to bothordersandprimary.orders, from translated offsets. Watch lag, error rates, and DLTs. Communicate the RPO: events produced in the last seconds before the outage may be missing until the primary returns. - 4Duplicates and gaps
What do consumers see right after failover?
Write your answer, then reveal
Duplicates: translated offsets can be a little behind, so recent records are processed again. Idempotency by event ID (and unique constraints on effects like charges) makes that safe. Gaps: records not yet mirrored when the region died. They are not lost forever if the primary's disks survive; after recovery, reconcile by comparing event IDs or replaying the missing range from the old cluster.
- 5The databases behind the services
The order service's outbox lives in PostgreSQL in the primary region. What happens to it?
Write your answer, then reveal
Kafka is not the system of record; the databases are. Replicate them to the backup region (asynchronous replicas, promoted on failover). After promotion, the outbox relay (Debezium) starts against the new primary database. Some outbox rows may be published twice (already mirrored, then relayed again), which idempotent consumers absorb. Daily reconciliation between the ledger and downstream projections catches anything else.
- 6Failing back
The primary region is healthy again. Can you switch everything back now?
Write your answer, then reveal
No. The backup cluster holds writes the primary never saw. Start MirrorMaker 2 in the reverse direction, wait until lag is zero and checkpoints are current, then move producers back in a quiet window, then consumers from translated offsets, and finally restore the original replication direction. Fail back as deliberately as you failed over.
- 7Proving it works
How do you know the runbook works before a real outage?
Write your answer, then reveal
Scheduled game days: fail over a non-critical service first, then the whole platform, measuring actual RPO (missing events) and RTO (time to healthy). Alert continuously on replication lag, checkpoint age, and MirrorMaker 2 task health, because a DR copy that silently stopped a week ago is worse than none.
C9.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| MirrorMaker 2 stalls | RPO grows silently | Alert on replication lag and checkpoint latency |
| Schemas not in backup | Consumers cannot deserialize | Replicate the registry with the same IDs; test it |
| Split brain (both regions writing) | Conflicting updates | Fence the old region before switching producers |
| Duplicate effects after failover | Double charges | Idempotency keys and unique constraints at the effect |
| Hot standby never exercised | Failover fails on the day | Game days, automated runbooks |
C9.7What a strong answer shows
SDE-2: separates zone and region failures, chooses active-passive with MirrorMaker 2, explains offset translation and duplicates, and makes consumers idempotent. Senior adds: the stretch cluster trade-off and the third-region requirement, everything that must be replicated besides data, database promotion and outbox replay, fencing against split brain, failback through reverse replication, and game days with measured RPO and RTO.
C9.8Follow-up questions
- Why run MirrorMaker 2 in the backup region rather than the primary?
Reveal
Consuming across the region boundary and producing locally keeps the writes, which need acknowledgements, on the low-latency side, and if the primary region dies the replication tooling is not lost with it.
- How would active-active change the consumers?
Reveal
Each region writes local topics and mirrors the other's under a prefix, so consumers read both (
ordersandother.orders) and must handle the same entity being updated in two regions, usually by giving each entity a home region or by merging with versions. - Can you guarantee zero duplicates after failover?
Reveal
Not with asynchronous replication and translated offsets. Design for duplicates with idempotent consumers and unique constraints, and measure how many occur in game days.
- What would you monitor for the DR setup itself?
Reveal
Replication lag per topic, checkpoint latency, MirrorMaker 2 task failures, standby consumer readiness, schema registry replication, and the age of the last successful failover test.
Case study 10: Order saga with inventory reservation
Placing an order touches three services: reserve stock, authorize the payment, confirm the order, and undo the earlier steps if a later one fails. There is no distributed transaction, messages can be late or duplicated, and a payment can time out without saying whether it succeeded.
Design checkout across an order service, an inventory service, and a payment service using Kafka. Cover choreography versus orchestration, the topics and keys, how each step is published reliably, compensations, timeouts and the "did the payment go through?" problem, late or duplicate messages, overselling during a flash sale, and how support finds a stuck order.
Reveal the answer
A strong answer uses an orchestrated saga: the order service keeps a state machine per order and sends commands through an outbox; participants reply on a replies topic; everything is keyed by order_id; every command and compensation is idempotent; timeouts come from a scheduler and resolve ambiguity by querying the payment provider with the same idempotency key before compensating; a compensation that arrives before its forward action leaves a marker so the late action becomes a no-op; inventory prevents overselling with conditional updates in its own database; and a saga table plus a stuck-saga alert make every order traceable.
C10.1Requirements
Functional
- Checkout: create order (PENDING), reserve stock, authorize payment, confirm order (CONFIRMED).
- If stock is unavailable: reject the order. If payment fails: release stock, cancel the order.
- Users and support can see where an order is.
Non-functional
- Never oversell; never charge for a cancelled order; no reservation stuck forever.
- 2,000 orders per second during flash sales.
- A slow payment provider must not block other orders.
C10.2Estimates
| Quantity | Calculation | Result |
|---|---|---|
| Orders at peak | given | 2,000/s |
| Messages per order | 2 commands, 2 replies, sometimes compensations | about 5 |
| Message rate | 2,000 x 5 | about 10,000/s |
| Message size | small JSON or Avro | about 1 KB |
| Open sagas | 2,000/s x about 5 s average duration | about 10,000 at once |
| Reservation expiry | business rule | 10 minutes |
C10.3Topics and events
| Topic | Key | Producer | Consumer |
|---|---|---|---|
inventory.commands.v1 |
order_id |
Order service (outbox) | Inventory service |
payment.commands.v1 |
order_id |
Order service (outbox) | Payment service |
order.saga.replies.v1 |
order_id |
Inventory and payment (outboxes) | Order service |
orders.events.v1 |
order_id |
Order service (outbox) | Notifications, analytics |
- Commands (
ReserveStock,ReleaseStock,AuthorizePayment,VoidPayment) have one owner each; replies (StockReserved,OutOfStock,PaymentAuthorized,PaymentDeclined) go back to the orchestrator; events announce outcomes to everyone else. - Key by
order_ideverywhere: a reserve and its release for the same order are in one partition, in order, and the orchestrator processes one order's replies in sequence.
C10.4High-level design
- The order service inserts the order (PENDING), a saga row (step = RESERVING), and a
ReserveStockoutbox row in one database transaction. - Inventory reserves stock in its own database (with a conditional update) and replies through its outbox:
StockReservedorOutOfStock. - On
StockReserved, the orchestrator moves to AUTHORIZING and sendsAuthorizePayment; onOutOfStock, it cancels the order. - On
PaymentAuthorized, it confirms the order and publishesOrderConfirmed. OnPaymentDeclined, it sendsReleaseStockand cancels. - A scheduler fires a timeout if a step gets no reply in time.
C10.5Deep dives
Guided designDeep divesAnswer each before revealing
- 1Orchestration or choreography?
Why not let each service react to the previous service's event?
Write your answer, then reveal
Choreography (inventory listens for
OrderCreated, payment listens forStockReserved, inventory listens forPaymentDeclined) is fine for two steps. With more steps the workflow exists only as a web of subscriptions: nobody owns timeouts, and nobody can answer "why is this order stuck?". An orchestrator keeps the state machine in one place, makes compensations explicit, and gives support one table to look at. - 2Publishing each step reliably
The orchestrator updates the saga row and must send the next command. How do you avoid the dual-write problem?
Write your answer, then reveal
The saga state change and the outgoing command are written in the same database transaction, the command as an outbox row that CDC or a poller publishes. Participants do the same with their replies. Every hop is then at-least-once, so every handler must be idempotent.
- 3Idempotent steps
ReserveStockfor order 42 is delivered twice. What happens?Write your answer, then reveal
Inventory records reservations keyed by
order_idwith a unique constraint: the second command finds the existing reservation and re-sends the same reply. The orchestrator also ignores replies that do not match the current step (a duplicateStockReservedwhile already AUTHORIZING changes nothing). Payment usesorder_idas the provider's idempotency key, so a retried authorization cannot charge twice. - 4The ambiguous timeout
AuthorizePaymentwas sent 30 seconds ago and nothing came back. Release the stock and cancel?Write your answer, then reveal
Not blindly: the payment may have succeeded, and cancelling would leave a charge on a cancelled order. On timeout, the orchestrator asks the payment service for the status of that
order_id(which asks the provider using the same idempotency key). Authorized: continue. Declined or never received: compensate. Still unknown: retry the query with backoff, then void the authorization if it appears later. Timeouts come from a scheduler (a store of due times), not from sleeping threads. - 5Late and reordered messages
A timeout triggered
ReleaseStock, and then the originalReserveStockarrives late (for example after a retry from the outbox relay). What protects you?Write your answer, then reveal
Both are keyed by
order_id, so within the topic they arrive in the order they were published. If a release still overtakes a reserve (for example after a replay), inventory records a CANCELLED marker for that order when it releases something it never reserved, so the late reserve sees the marker and does nothing. Compensations must be safe to run before, after, or twice relative to their forward action. - 6Overselling in a flash sale
10,000 orders for 500 units of one SKU arrive in a minute. How does inventory avoid overselling, and is that SKU a hot key?
Write your answer, then reveal
Kafka is keyed by
order_id, so load spreads across partitions; correctness lives in the inventory database:UPDATE stock SET reserved = reserved + :q WHERE sku = :sku AND total - reserved >= :q, and zero updated rows means out of stock. The single SKU row becomes a contention point, so very hot items can split stock into several buckets (rows) or use an atomic counter in a fast store, reconciled with the database. Reservations carry an expiry so abandoned ones return to stock. - 7Finding a stuck order
A customer says their order has shown "Processing" for an hour.
Write your answer, then reveal
The saga table holds the current step, last update time, and history per order. A job alerts on sagas older than their step's timeout, and an admin action can retry the step or compensate. Trace IDs in message headers connect the order's messages across services in tracing tools.
C10.6Failures and bottlenecks
| Failure | Effect | Mitigation |
|---|---|---|
| Payment provider slow | Sagas wait; reservations held | Timeouts with status queries; reservation expiry |
| Duplicate commands or replies | Double reservation or charge | Idempotency keys, unique constraints, state checks |
| Release before reserve | Stock leaked | CANCELLED marker makes the late reserve a no-op |
| Hot SKU | Row contention in inventory | Stock buckets or atomic counters with reconciliation |
| Orchestrator down | Sagas pause | Stateless instances over the saga table; resume from state on restart |
C10.7What a strong answer shows
SDE-2: orchestrated saga with an explicit state machine, outbox for every step, keys by order_id, idempotent commands and compensations, and no distributed transaction. Senior adds: ambiguous timeouts resolved by status queries with idempotency keys, compensations that commute with late forward actions, reservation expiry and hot-SKU contention, and operability through a saga table and stuck-saga alerts.
C10.8Follow-up questions
- Why not use Kafka transactions across the three services?
Reveal
Each service changes its own database; Kafka transactions only make Kafka writes and offsets atomic. Local transactions plus outboxes and idempotent handlers are what make each step reliable.
- Should commands go through Kafka at all, or be direct calls?
Reveal
Either works. Kafka commands decouple the orchestrator from participant availability and give buffering and replay; direct calls give immediate answers but need their own retries. Many systems call payment synchronously and use Kafka for everything else.
- How do you show the user a result quickly if the saga is asynchronous?
Reveal
Return the order as PENDING immediately and push or poll the final status; most sagas finish in seconds, and the UI shows "Processing" with a clear final state.
- How would you add a new step, such as a fraud check, without breaking orders in flight?
Reveal
Version the saga definition: orders started on the old version finish on it, new orders use the new state machine, and the orchestrator handles both until old sagas drain.
Cheat sheets
Defaults and commands you look up most, checked against Kafka 4.3. Defaults are what you get when you set nothing; the last column says when to change them.
Producer settings
| Setting | Default | Change it when |
|---|---|---|
acks |
all |
Rarely. 1 only for data you can afford to lose |
enable.idempotence |
true |
Keep on. It is silently turned off if you set acks other than all or retries=0 without setting it explicitly |
retries |
Integer.MAX_VALUE |
Leave it; bound retrying with delivery.timeout.ms |
delivery.timeout.ms |
120000 (2 min) | Total time for a send, including retries. Must be at least linger.ms + request.timeout.ms |
request.timeout.ms |
30000 | Rarely |
max.in.flight.requests.per.connection |
5 | Keep at most 5 with idempotence (larger values are rejected unless idempotence is disabled) |
batch.size |
16384 (16 KB) | Raise to 64 to 256 KB for throughput |
linger.ms |
5 (since 4.0; 0 before) | 10 to 100 for throughput; 0 for lowest latency |
compression.type |
none |
lz4 or zstd for most workloads |
buffer.memory |
33554432 (32 MB) | Raise for many partitions or large bursts |
max.block.ms |
60000 | How long send() may block on a full buffer or missing metadata |
max.request.size |
1048576 (1 MB) | Large records (raise the topic's max.message.bytes too) |
retry.backoff.ms / retry.backoff.max.ms |
100 / 1000 | Rarely |
partitioner.ignore.keys |
false |
Only if you do not need per-key order |
transactional.id |
none | Set for transactions; must be stable per producer instance |
transaction.timeout.ms |
60000 | Long transactions (broker cap: transaction.max.timeout.ms, 15 min) |
Consumer settings
| Setting | Default | Change it when |
|---|---|---|
group.id |
none | Required for subscribe and commits |
group.protocol |
classic |
consumer to use the new protocol (KIP-848, Kafka 4.0+ brokers) |
enable.auto.commit |
true |
Set false and commit after processing (Spring does this for you) |
auto.commit.interval.ms |
5000 | Rarely |
auto.offset.reset |
latest |
earliest for pipelines that must see all data; by_duration:PT1H style since 4.0; none to fail loudly |
max.poll.records |
500 | Lower if processing a batch can exceed max.poll.interval.ms |
max.poll.interval.ms |
300000 (5 min) | Max time between poll() calls before the member is removed |
session.timeout.ms |
45000 | Classic protocol only; with the new protocol the broker's group.consumer.session.timeout.ms (45 s) applies |
heartbeat.interval.ms |
3000 | Classic protocol only; new protocol uses the broker's group.consumer.heartbeat.interval.ms (5 s) |
fetch.min.bytes / fetch.max.wait.ms |
1 / 500 | Raise fetch.min.bytes for throughput |
max.partition.fetch.bytes |
1048576 (1 MB) | Large records |
fetch.max.bytes |
52428800 (50 MB) | Rarely |
isolation.level |
read_uncommitted |
read_committed when reading transactional output |
partition.assignment.strategy |
[RangeAssignor, CooperativeStickyAssignor] |
Classic protocol: CooperativeStickyAssignor alone for incremental rebalances |
group.instance.id |
none | Static membership: avoid rebalances on restarts |
client.rack |
none | Fetch from the closest replica (with a broker replica selector) |
Broker and topic settings
| Setting (topic-level name) | Default | Notes |
|---|---|---|
log.retention.hours (retention.ms) |
168 h (7 days) | -1 means forever |
log.retention.bytes (retention.bytes) |
-1 | Per partition |
log.segment.bytes (segment.bytes) |
1 GiB | Retention deletes whole segments |
log.roll.hours (segment.ms) |
168 h | Lower it for faster retention or compaction on quiet topics |
log.cleanup.policy (cleanup.policy) |
delete |
compact or compact,delete |
min.insync.replicas |
1 | Set 2 with RF 3 for important topics |
default.replication.factor |
1 | Set 3 in production |
num.partitions |
1 | Default for auto-created topics |
auto.create.topics.enable |
true |
Turn off in production |
unclean.leader.election.enable |
false |
Keep off unless availability beats data |
replica.lag.time.max.ms |
30000 | When a follower leaves the ISR |
message.max.bytes (max.message.bytes) |
1048588 | Largest record batch accepted |
compression.type |
producer |
Keep the producer's codec |
delete.retention.ms |
86400000 (1 day) | How long tombstones stay in compacted topics |
min.cleanable.dirty.ratio |
0.5 | Compaction trigger |
message.timestamp.after.max.ms |
3600000 (1 h, since 4.0) | Rejects records timestamped too far in the future |
offsets.retention.minutes |
10080 (7 days) | Committed offsets of empty groups expire after this |
group.initial.rebalance.delay.ms |
3000 | Waits for more members before the first classic rebalance |
broker.session.timeout.ms |
9000 | Controller fences a broker that stops heartbeating |
Spring for Apache Kafka defaults
| Behavior | Default |
|---|---|
| Offset commits | AckMode.BATCH: after all records from a poll are processed; Kafka auto-commit is turned off |
| Error handler | DefaultErrorHandler: 10 attempts with no delay, then log and skip |
| Dead letter destination | DeadLetterPublishingRecoverer: <topic>-dlt, same partition (3.3+; <topic>.DLT before) |
@RetryableTopic |
3 attempts, topics suffixed -retry and -dlt |
| JSON (de)serializers | JacksonJsonSerializer and JacksonJsonDeserializer in 4.x (JsonSerializer and JsonDeserializer are deprecated) |
Delivery recipes
| Goal | Producer | Consumer |
|---|---|---|
| At-most-once | acks=0 or 1 |
Commit before processing |
| At-least-once | acks=all, idempotence on |
Auto-commit off, commit after processing |
| Effectively once into a database | acks=all, idempotence on |
Idempotent writes, or store offsets in the same database transaction |
| Exactly-once, Kafka to Kafka | transactional.id, sendOffsetsToTransaction |
isolation.level=read_committed |
| Exactly-once in Kafka Streams | processing.guarantee=exactly_once_v2 |
Handled by Streams |
| Database change plus event | Transactional outbox and CDC | Idempotent consumers |
Command line
Run from /opt/kafka/bin in the container, adding --bootstrap-server localhost:9092 (and --command-config for secured clusters).
kafka-topics.sh --list
kafka-topics.sh --describe --topic orders
kafka-topics.sh --create --topic orders --partitions 12 --replication-factor 3 --config min.insync.replicas=2
kafka-topics.sh --alter --topic orders --partitions 24 # keys will move
kafka-topics.sh --describe --under-replicated-partitions
kafka-topics.sh --describe --under-min-isr-partitions
kafka-configs.sh --describe --entity-type topics --entity-name orders
kafka-configs.sh --alter --entity-type topics --entity-name orders --add-config retention.ms=259200000
kafka-get-offsets.sh --topic orders --time latest # or earliest, or a timestamp in mskafka-console-producer.sh --topic orders --reader-property parse.key=true --reader-property key.separator=:
kafka-console-consumer.sh --topic orders --from-beginning \
--formatter-property print.key=true --formatter-property print.partition=true --formatter-property print.offset=true
kafka-console-consumer.sh --topic orders --partition 3 --offset 1200 --max-messages 5
kafka-console-share-consumer.sh --topic jobs --group workerskafka-consumer-groups.sh --list
kafka-consumer-groups.sh --describe --group billing # lag per partition
kafka-consumer-groups.sh --describe --group billing --members --verbose
kafka-consumer-groups.sh --describe --group billing --state
kafka-consumer-groups.sh --group billing --topic orders --reset-offsets --to-datetime 2026-10-17T00:00:00.000 --dry-run
kafka-consumer-groups.sh --group billing --topic orders --reset-offsets --to-earliest --execute # group must be empty
kafka-consumer-groups.sh --group billing --topic orders:3 --reset-offsets --shift-by 1 --execute # skip one record
kafka-share-groups.sh --describe --group workers --memberskafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status
kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --replication
kafka-features.sh --bootstrap-server localhost:9092 describe
kafka-log-dirs.sh --describe --topic-list orders
kafka-leader-election.sh --election-type preferred --all-topic-partitions
kafka-reassign-partitions.sh --reassignment-json-file plan.json --execute --throttle 10000000
kafka-reassign-partitions.sh --reassignment-json-file plan.json --verify # also removes the throttle
kafka-transactions.sh --bootstrap-server localhost:9092 list
kafka-dump-log.sh --files /tmp/kraft-combined-logs/orders-0/00000000000000000000.log --print-data-logkafka-producer-perf-test.sh --topic bench --num-records 1000000 --record-size 512 --throughput -1 \
--command-property acks=all --command-property linger.ms=20 --command-property compression.type=lz4
kafka-consumer-perf-test.sh --topic bench --num-records 1000000 --group bench-readerWhat happens when
Assume RF 3 and min.insync.replicas=2 unless stated.
| Event | Producers | Consumers |
|---|---|---|
| One broker down | acks=all writes continue after leaders move |
Continue after leaders move |
| Two brokers down | acks=all gets NotEnoughReplicasException (retried until delivery.timeout.ms); acks=1 writes are accepted |
No new records visible: the high watermark does not advance while ISR is below min ISR |
Leader dies right after an acks=1 write |
Write may be lost; the old leader truncates it on return | Never saw it |
| All replicas down | Partition offline | Wait for an in-sync (or eligible) replica to return |
| Consumer crashes after processing, before commit | Records reprocessed: duplicates | |
| Consumer commits, then crashes before processing | Records skipped: loss | |
Processing exceeds max.poll.interval.ms |
Member removed; partitions reassigned; its commit fails | |
| Committed offset deleted by retention | auto.offset.reset decides where to start |
|
| Group empty for 7 days | Offsets expire; auto.offset.reset applies on return |
|
| Producer retries without idempotence | Duplicates, and reordering if more than 1 request in flight | Sees duplicates |
| Producer buffer full | send() blocks up to max.block.ms, then fails with TimeoutException |
Errors you will meet
| Error | Meaning | What to do |
|---|---|---|
NotEnoughReplicasException |
ISR below min.insync.replicas for acks=all |
Retriable; find out why replicas are down or lagging |
NotLeaderOrFollowerException |
Client metadata is stale | Retriable; the client refreshes metadata |
TimeoutException (producer) |
Not sent within delivery.timeout.ms or max.block.ms |
Check broker health, network, and buffer pressure |
RecordTooLargeException |
Record exceeds max.request.size or max.message.bytes |
Raise both limits, compress, or store the payload elsewhere |
SerializationException |
Serializer failed | Not retriable; fix data or schema |
RecordDeserializationException |
Consumer cannot deserialize a record | Poison pill: it carries partition and offset, so skip or dead-letter it (ErrorHandlingDeserializer in Spring) |
CommitFailedException |
Commit after the member was removed from the group | Shorten processing per poll or raise max.poll.interval.ms |
RebalanceInProgressException |
Commit during a rebalance | Poll again, then commit |
ProducerFencedException |
Another producer took over the transactional.id |
Fatal: close this producer |
OutOfOrderSequenceException |
Broker saw a sequence gap from an idempotent producer | Fatal for the producer; investigate data loss on the broker side |
UnknownTopicOrPartitionException |
Topic does not exist | Create it; check the name and auto-creation settings |
TopicAuthorizationException, GroupAuthorizationException |
ACL missing | Grant the right operation to the principal |
WakeupException |
You called consumer.wakeup() |
Expected during shutdown; close the consumer |
Rules of thumb
- Partitions: enough for peak throughput and consumer parallelism with growth headroom; changing the count later remaps keys. Use counts with many divisors (6, 12, 24, 48).
- Replication: RF 3,
min.insync.replicas=2,acks=all, unclean election off, replicas spread across zones. - Consumers per group: at most the partition count (except share groups).
- Lag: alert on lag in time or on growth, not on a fixed record count.
- Record size: keep records small (KBs); store large payloads in object storage and send a reference.
- Controllers: 3 (or 5) KRaft controllers; brokers scale independently.
Kafka in interviews
Where Kafka comes up in SDE-2 interviews, how to use it well in a design round, how to talk about your own Kafka work, and a bank of questions with answers to rehearse out loud.
Where Kafka comes up
- System design rounds: any design with asynchronous work, fan-out, event-driven services, pipelines, or audit trails. You are expected to place Kafka well and defend the details, not just draw a box labelled "Kafka".
- Deep-dive questions: "How does Kafka guarantee ordering?", "What happens when a broker dies?", "How do you get exactly-once?" These test whether you know the mechanism behind the claim.
- Experience discussions: "Tell me about a system you built with Kafka" or "Describe an incident". Interviewers probe your decisions: keys, partitions, delivery semantics, failure handling, monitoring.
- Coding rounds: rare. Occasionally a "design a message queue" or "implement a consumer with retries" exercise.
Using Kafka in a design round
Propose Kafka when you need durable, replayable, ordered (per key) streams consumed by several independent readers, or a buffer between producers and slower consumers. Prefer a simple queue or a direct call when you need request-response, per-message priority or delay, or tiny scale where Kafka's operational cost is not worth it.
Every time you draw Kafka, cover these five points in a sentence each:
| Point | Say |
|---|---|
| Topic and key | "One topic of order events keyed by order_id, so each order's events stay in order." |
| Partitions | "Peak 5,000 events/s; one consumer handles about 500/s, so at least 10; I will use 24 for growth." |
| Delivery semantics | "acks=all, idempotent producer, at-least-once consumers that are idempotent by event ID." |
| Failures | "Blocking retries for transient errors, a dead letter topic for poison records, alerts on lag in seconds." |
| Contract and retention | "Avro with a schema registry, BACKWARD compatibility; 7 days retention, enough to replay after a bad deploy." |
What separates answers: an SDE-2 answer names the key, the partition math, and the delivery guarantee, and handles duplicates. A senior answer also covers the dual-write problem (outbox), hot keys, rebalances, consumer lag in time, replay strategy, and the operational cost of each choice.
Decode any prompt
Most Kafka questions are a few recurring pressures in disguise. Train yourself to hear the signal and name the mechanism.
| Signal in the prompt | Pressure | Name this |
|---|---|---|
| "Never lose a confirmed payment or order" | Durability over write latency | acks=all, RF 3, min.insync.replicas=2, unclean election off, outbox for database changes |
| "Events for the same X must stay in order" | Per-entity ordering | Key by X; idempotent producer; blocking retries or per-key parking for failures |
| "Never apply the same event twice" | Effectively-once effects | At-least-once commits plus idempotent writes (unique event ID, upserts, versions) |
| "Read from A, transform, write to B" | Kafka-to-Kafka exactly-once | Transactions with sendOffsetsToTransaction and read_committed, or Kafka Streams exactly_once_v2 |
| "Write to the database and notify other services" | Dual writes | Transactional outbox with CDC or a poller |
| "Keep the latest state per entity", "rebuild a cache or index" | Current state, replay | Compacted topic keyed by entity ID; tombstones for deletes |
| "Calls an external API that sometimes fails" | Head-of-line blocking | Classify errors; retry topics and a DLT for some failures; pause and circuit breaker when the dependency is down |
| "One tenant or celebrity is half the traffic" | Hot key | Finer key if ordering allows; salting with two-stage aggregation; a separate lane for the big tenant |
| "Millions of events per second" | Throughput | Batching (linger.ms, batch.size), lz4 or zstd, partitions from per-partition and per-consumer rates |
| "Urgent messages must not wait behind bulk ones" | Priority | Separate topics and consumer pools per priority; Kafka has no message priority |
| "Run this in 10 minutes", "retry tomorrow" | Delays | Delay tiers or a scheduler with a store; Kafka has no per-message delay |
| "More workers than partitions; order does not matter" | Queue semantics | Share groups (Kafka 4.2+), or more partitions |
| "Consumers keep getting kicked out" | Poll loop too slow | Smaller max.poll.records, no blocking I/O in the loop, cooperative or KIP-848 rebalancing, static membership |
| "Must survive a region outage" | Disaster recovery | MirrorMaker 2 active-passive with translated offsets and idempotent consumers; stretch cluster only if zero loss beats latency |
Guided designRapid drills: 60 seconds eachAnswer each before revealing
- 1Bank ledger with SMS alerts
Every debit and credit must update the balance in order, never be lost or applied twice, and trigger an SMS. Key, durability, and consumers?
Write your answer, then reveal
Key by
account_idso an account's entries stay ordered. Publish through an outbox from the ledger database; topic with RF 3 andmin.insync.replicas=2, produceracks=allwith idempotence. Two consumer groups (balance projector, SMS sender), both at-least-once and idempotent by entry ID, the SMS sender also by a notification ID. - 2500,000 vehicle readings per second
Cars send GPS and engine data every 5 seconds; alert within 3 seconds if engine temperature stays above a limit for three readings. Partitioning and processing?
Write your answer, then reveal
Key by
vehicle_idso each car's readings are ordered and land in one task. Producers batch and compress (linger.msaround 20, lz4 or zstd). A Kafka Streams or Flink job keeps per-vehicle state in event time and emits alerts to an alerts topic; partitions sized from throughput per partition and per task. - 3Price change to search and cache within a second
A seller edits a price in PostgreSQL; search and Redis must follow. Why not write all three from the service, and what instead?
Write your answer, then reveal
Three writes can partly fail and leave systems out of sync. Use Debezium to stream row changes into a compacted topic keyed by product ID; one consumer group updates search with versioned upserts, another evicts cache keys. A new index is rebuilt by replaying the compacted topic.
- 4Webhooks to 10,000 merchants
Some merchant endpoints are slow or down. What goes wrong with retrying inside the main poll loop, and what do you build?
Write your answer, then reveal
One dead endpoint blocks every merchant behind it on that partition and can exceed
max.poll.interval.ms. Move failed deliveries to retry topics with growing delays and then a DLT, keep per-merchant rate limits and circuit breakers, and key by merchant so one merchant's webhooks stay ordered. A share group fits if ordering per merchant is not required.
Talking about your own Kafka work
Prepare two stories: one system you built or changed, and one incident. For each, be ready to answer:
- Why Kafka and not a queue, a database table, or a direct call.
- Topic and key design, partition count, and what you would change now.
- Delivery guarantees: producer settings, commit strategy, and how duplicates are handled.
- Failure handling: retries, dead letters, poison records, and what happens during a rebalance.
- Observability: which metrics and alerts you used, and how you found the root cause.
- Numbers: throughput, lag, latency, retention, number of consumers.
A useful shape for an incident: what users saw, how you detected it, what you checked first and why, the root cause, the fix, and what you changed so it cannot recur. Module 21 has incident walkthroughs to borrow the reasoning from.
Question bank: fundamentals
- What is Kafka, in two sentences?
Reveal
A distributed, replicated, append-only log. Producers append records to partitioned topics, and any number of consumer groups read them independently at their own pace, from any retained position.
- How is Kafka different from a traditional message queue like RabbitMQ?
Reveal
A queue usually deletes a message once it is consumed and pushes messages to consumers. Kafka keeps records for a retention period regardless of consumption, consumers pull and track their own offsets, many groups can read the same data, and order is guaranteed per partition. Kafka 4.2+ also offers share groups for queue-style consumption.
- What guarantees ordering in Kafka?
Reveal
Records in one partition are stored and read in order. Records with the same key go to the same partition (while the partition count does not change), so ordering per key holds. There is no order across partitions.
- What is an offset?
Reveal
The position of a record within a partition, assigned by the partition leader when the record is appended. Consumers commit offsets to remember where they are.
- What is a consumer group?
Reveal
A set of consumers sharing a
group.id. Each partition is assigned to one member, so the group splits the work; separate groups each get all the data. - Why can't you have more active consumers than partitions in a group?
Reveal
Each partition goes to exactly one member of a consumer group, so extra members sit idle. Share groups are the exception: they hand out records, not partitions.
- What is KRaft?
Reveal
Kafka's built-in Raft-based metadata quorum. Controllers store cluster metadata in a replicated log and elect partition leaders. It replaced ZooKeeper, which was removed in Kafka 4.0.
- How long does Kafka keep data?
Reveal
Until retention removes it: by time (
retention.ms, 7 days by default) or size (retention.bytes), deleting whole segments. Compacted topics keep the latest record per key instead.
Question bank: producers
- What does
ackscontrol?Reveal
When the leader answers a produce request:
0never waits,1after the leader writes it,allafter every in-sync replica has it. Combined withmin.insync.replicas,acks=alldecides how many copies exist before success. - How does the idempotent producer prevent duplicates?
Reveal
The producer gets a producer ID and numbers each batch per partition. The leader remembers recent sequence numbers and discards a retried batch it already wrote, and rejects gaps, so retries cannot duplicate or reorder within a partition.
- What do
batch.sizeandlinger.msdo?Reveal
Records are grouped per partition into batches up to
batch.sizebytes;linger.msis how long the producer may wait to fill a batch. Bigger batches mean better throughput and compression, at a few milliseconds of latency. - How does the default partitioner pick a partition?
Reveal
An explicit partition wins; otherwise a keyed record goes to
murmur2(key bytes) % partitions; a keyless record sticks to one partition until a batch fills, then switches (adapting to slow brokers). - What happens if you add partitions to a keyed topic?
Reveal
New records for many keys map to different partitions, so per-key ordering breaks across the change and key-based state (such as Streams stores) no longer lines up. Plan partition counts up front, or migrate to a new topic.
- Your producer gets
TimeoutException. What do you check?Reveal
Whether brokers are reachable and leaders exist (metadata), whether the ISR is healthy, whether the buffer is full because the producer outpaces the cluster, and whether
delivery.timeout.msis too short for the retry pattern. - How would you handle a 10 MB message?
Reveal
Prefer storing the payload in object storage and sending a reference (claim check). If it must go through Kafka, raise
max.request.size, the topic'smax.message.bytes, and consumer fetch sizes, and use compression.
Question bank: consumers and groups
- What triggers a rebalance?
Reveal
A member joining or leaving (including being removed after missing heartbeats or exceeding
max.poll.interval.ms), a change in subscriptions, or a change in the partitions of subscribed topics (more partitions, or new topics matching a pattern). - What is the difference between eager and cooperative rebalancing?
Reveal
Eager: every member gives up all partitions, then receives a new assignment, so the whole group stops. Cooperative: only the partitions that move are revoked, so others keep processing. The new consumer protocol (KIP-848) is incremental and driven by the broker.
- What is
max.poll.interval.msand why does it matter?Reveal
The longest allowed gap between
poll()calls (5 minutes by default). Exceed it and the member is removed from the group and its partitions are reassigned, causing duplicates and rebalance loops. - How do you process records with a slow downstream without being kicked out?
Reveal
Fewer records per poll, batch the downstream writes,
pause()partitions while waiting (still callingpoll()), process in parallel while preserving per-key order, or scale out up to the partition count. - What is static membership?
Reveal
Setting
group.instance.idso a restarting consumer keeps its assignment if it returns within the session timeout, avoiding rebalances during rolling restarts. - When would you use a share group?
Reveal
For independent jobs where you want more workers than partitions, per-record acknowledgement, and delivery counting, and where per-key order does not matter.
- How do you run a consumer that must process partitions in parallel but keep per-key order?
Reveal
Hand records to worker threads by hashing the key, so each key always goes to the same worker; commit an offset only when all records below it in that partition are done.
Question bank: delivery and transactions
- Explain at-most-once, at-least-once, and exactly-once in terms of commits.
Reveal
At-most-once: commit before processing; a crash skips records. At-least-once: commit after processing; a crash reprocesses records. Exactly-once: offsets and outputs commit atomically (Kafka transactions), or processing is idempotent so repeats change nothing.
- Does Kafka give exactly-once delivery end to end?
Reveal
Within Kafka, yes: transactions make consume-process-produce atomic, and
read_committedconsumers see only committed data. Effects outside Kafka (a database, an email) need idempotency or the outbox pattern. - How do Kafka transactions work at a high level?
Reveal
A producer with a
transactional.idregisters with a transaction coordinator, writes to partitions, adds the consumer offsets withsendOffsetsToTransaction, then commits. The coordinator writes commit or abort markers to every partition involved;read_committedconsumers read up to the last stable offset and skip aborted records. - What is zombie fencing?
Reveal
When a producer with the same
transactional.idstarts, the coordinator bumps the epoch, and requests from the old instance (a "zombie" after a pause or network split) are rejected withProducerFencedException. - What is the outbox pattern and why use it?
Reveal
Write the business change and an event row in one database transaction; a relay (CDC or a poller) publishes the event to Kafka. It removes the dual-write problem where a crash leaves the database and Kafka inconsistent.
- How do you make a consumer idempotent?
Reveal
Record processed event IDs (or use a natural unique key) in the same transaction as the side effect, with a unique constraint, and skip duplicates. Or design writes as upserts keyed by entity and version.
- A consumer must write to a database and commit offsets. How do you avoid duplicates?
Reveal
Either make writes idempotent and commit offsets after, or store the offsets in the same database transaction as the writes and seek to them on startup.
Question bank: replication and storage
- What is the ISR?
Reveal
The in-sync replica set: the leader and followers that are caught up (not lagging more than
replica.lag.time.max.ms). Withacks=all, a write succeeds once all ISR members have it. - What is the high watermark?
Reveal
The highest offset replicated to all ISR members. Consumers only read below it, so they never see a record that could be lost in a failover.
- With RF 3 and
min.insync.replicas=2, what happens when two brokers fail?Reveal
The ISR drops to 1, so
acks=allwrites fail withNotEnoughReplicasExceptionand the high watermark stops advancing. Reads of already committed data continue. Availability is traded for durability. - What is unclean leader election?
Reveal
Electing a replica that is not in the ISR when no in-sync replica is available. It restores availability but loses committed records the new leader lacks, so it is off by default.
- Why is Kafka fast?
Reveal
Sequential appends to segment files, heavy use of the page cache, batching and compression end to end, zero-copy transfer from page cache to socket, and partitions spreading load across brokers and disks.
- Does Kafka fsync every write?
Reveal
No, by default it relies on replication for durability and lets the OS flush. A record acknowledged with
acks=allis in memory on several machines. - What is log compaction?
Reveal
A cleanup policy that keeps at least the latest record for each key and removes older ones; a null value (tombstone) deletes a key. Used for state, changelogs, and
__consumer_offsets. - What is tiered storage?
Reveal
Moving older, closed segments to object storage while keeping recent data on broker disks, so retention can grow without adding brokers. Consumers read old data transparently.
Question bank: design and operations
- How do you choose a partition key?
Reveal
The entity whose events must stay in order (an account, an order). Check its cardinality and skew: too few distinct keys or one dominant key creates hot partitions.
- How many partitions would you create?
Reveal
Enough for peak throughput divided by per-partition and per-consumer capacity, with growth headroom, since increasing later moves keys. Avoid thousands without need; each partition costs files, memory, and failover time.
- How do you handle a hot key?
Reveal
Find whether ordering for that key is truly needed. If not, salt the key. If yes, scale the consumer for that key (more efficient processing) or split the entity. For aggregations, pre-aggregate with salted keys and combine.
- How do you monitor consumer lag?
Reveal
From outside the consumer (Burrow, an exporter, or the broker's group offsets), per partition, and as time behind the head, alerting on growth or on age rather than a fixed record count.
- How do you replay data after a bug?
Reveal
Stop the consumer group, reset its offsets (
--to-datetime), deploy the fix, restart. Processing must be idempotent. For data beyond retention, replay from an archive. - How do you evolve an event schema safely?
Reveal
Use a schema registry with a compatibility rule (BACKWARD by default): add optional fields with defaults, never reuse or retype fields, and use a new topic for breaking changes.
- How would you replicate Kafka across regions?
Reveal
MirrorMaker 2 (or a managed equivalent) copies topics and translates consumer offsets to a standby cluster. Plan for duplicates on failover, which idempotent consumers absorb.
- How would you secure a Kafka cluster?
Reveal
TLS on listeners, authentication with mTLS or SASL (SCRAM or OAUTHBEARER), ACLs with least privilege per principal, quotas against noisy clients, and encryption at rest.
Question bank: streams and ecosystem
- Kafka Streams or Flink?
Reveal
Kafka Streams is a library inside your service: Kafka in, Kafka out, no extra cluster, ideal for per-key stateful processing in Java services. Flink is a separate cluster with richer windowing, many sources and sinks, very large state, and SQL. Pick by where the data lives and who operates it.
- What is the difference between a KStream and a KTable?
Reveal
A KStream is a sequence of independent events; a KTable is the latest value per key, where each record is an update. Stream-table duality: a table is a stream compacted to the latest values; a table's changes form a stream.
- How does Kafka Streams keep state fault tolerant?
Reveal
State stores (RocksDB by default) are backed by compacted changelog topics. A restarted or moved task restores its store from the changelog; standby replicas shorten restore time.
- What is a grace period in windowing?
Reveal
How long a window keeps accepting late records after it ends. Records later than the grace period are dropped.
- What is Kafka Connect for?
Reveal
Moving data between Kafka and other systems with configuration instead of code: source connectors (databases, CDC with Debezium) and sink connectors (search, object storage, warehouses), with scaling, offsets, and dead letter queues built in.
- What does Debezium do?
Reveal
Reads a database's change log (for example the PostgreSQL WAL) and publishes every committed row change to Kafka, in commit order, keyed by primary key.
Question bank: scenarios
- Lag is growing on one partition only. What do you suspect?
Reveal
A hot key or a poison record stuck in retries. Check bytes per partition and the consumer's error logs for that partition.
- Consumers rebalance every few minutes. Where do you look?
Reveal
Processing time per poll versus
max.poll.interval.ms, long GC pauses, consumers crashing, rolling deploys without static membership, and the eager protocol stopping the whole group. - Downstream reports duplicate payments. Kafka is involved. How do you investigate and fix?
Reveal
Find where duplicates enter: producer retries without idempotence, consumer reprocessing after rebalances or crashes, or upstream retries. Fix with idempotent producers and an idempotency key enforced by a unique constraint at the payment write.
- Messages seem lost. What could cause it?
Reveal
acks=1or0with a leader failure, unclean election, consumer commits before processing, auto-commit with async processing, retention deleting data before slow consumers read it,auto.offset.reset=lateston a new group, or errors swallowed in producer callbacks. - A broker's disk is almost full. What do you do?
Reveal
Find the largest topics and partitions, lower retention on topics that allow it, delete unused topics, check for stuck compaction, then add disk or brokers and rebalance partitions. Alert earlier next time.
- You need to change a topic's key. How?
Reveal
Create a new topic with the new key, dual-publish or republish with a bridge, migrate consumers, then retire the old topic. A key change in place breaks ordering and co-partitioning.
Red flags
Common mistakes interviewers notice
- Drawing Kafka without naming the key, the partition count, or the delivery guarantee.
- Claiming "exactly-once" for side effects outside Kafka without idempotency.
- Assuming global ordering across partitions.
- Using Kafka for request-response, per-message delays, or priority without saying how.
- Ignoring duplicates on retries and rebalances.
- Adding partitions later as if keys would not move.
- Measuring lag only in records, or not monitoring it at all.
Glossary
Every Kafka term used in this handbook, in one or two lines. Use the search box to jump to a term.
- acks
- Producer setting for when the leader answers:
0(no wait),1(leader wrote it),all(every in-sync replica has it). Defaultall. - Assignor
- The strategy that maps partitions to group members, such as range, round robin, cooperative sticky, or the broker-side
uniformandrangeassignors of the new protocol. - At-least-once
- Every record is processed, possibly more than once. Commit after processing.
- At-most-once
- Every record is processed zero or one time. Commit before processing.
- Auto commit
enable.auto.commit=true: the consumer commits the latest polled offsets everyauto.commit.interval.ms(5 s) duringpoll().- auto.offset.reset
- Where a group starts when it has no valid committed offset:
latest(default),earliest,by_duration:<ISO 8601 duration>, ornone. - Batch
- A group of records for one partition sent and stored together. Compression and idempotence work per batch.
- Bootstrap servers
- The initial broker addresses a client uses to fetch cluster metadata. They need not be all brokers.
- Broker
- A Kafka server that stores partitions and serves produce and fetch requests.
- Changelog topic
- A compacted topic that backs a Kafka Streams state store so it can be restored.
- Claim check
- Storing a large payload elsewhere (object storage) and sending a reference through Kafka.
- Classic protocol
- The original consumer group protocol, where a group leader computes the assignment in the client. Still the client default in Kafka 4.x.
- Cleanup policy
- Topic setting
cleanup.policy:delete(retention by time or size),compact(latest per key), or both. - Cluster
- A set of brokers and controllers sharing metadata.
- Co-partitioning
- Two topics with the same key and partition count, so records with the same key land on the same partition number. Required for local joins.
- Commit
- Saving a consumer group's position for a partition in
__consumer_offsets. The committed offset is the next record to read. - Compaction
- Removing older records for a key while keeping at least the latest one. A null value (tombstone) deletes the key.
- Connector
- A Kafka Connect plugin that moves data from a system into Kafka (source) or from Kafka to a system (sink).
- Consumer
- A client that reads records by polling partitions.
- Consumer group
- Consumers sharing a
group.idthat split a topic's partitions between them; each partition goes to one member. - Consumer lag
- How far a group is behind: log end offset minus committed offset, per partition. Best watched as time.
- __consumer_offsets
- The internal compacted topic (50 partitions by default) where consumer groups store committed offsets.
- Controller
- A KRaft node that stores cluster metadata and makes decisions such as leader elections. One is active at a time.
- Cooperative rebalancing
- Incremental rebalancing where only partitions that move are revoked, so the rest keep processing.
- Dead letter topic (DLT)
- A topic where records that cannot be processed are sent, with error details, for inspection and replay.
- Debezium
- A set of source connectors that turn database change logs into Kafka records (change data capture).
- Delivery timeout
delivery.timeout.ms(2 min): the maximum time a producer spends trying to deliver a record, including retries.- Dual-write problem
- Updating a database and Kafka separately, where a crash between the two leaves them inconsistent.
- Eager rebalancing
- Rebalancing where every member revokes all partitions before reassignment, pausing the whole group.
- Eligible leader replicas (ELR)
- Replicas that left the ISR while it was below
min.insync.replicasand therefore still hold all committed data; they may be elected leader. Kafka 4.0+, on by default for new clusters since 4.1. - Epoch
- A counter that increases on each leadership or ownership change (leader epoch, producer epoch, member epoch) and fences stale actors.
- Event
- A fact that something happened. Stored in Kafka as a record.
- Exactly-once semantics (EOS)
- Each record's effect happens once. In Kafka, via idempotent producers and transactions; outside Kafka, via idempotent processing.
- Fetch
- A request to read records from a partition starting at an offset. Followers and consumers both fetch.
- Follower
- A replica that copies a partition from its leader.
- Follower fetching
- Consumers reading from a nearby follower instead of the leader, using
client.rackand a rack-aware replica selector. - Grace period
- How long a time window accepts late records after it ends.
- Group coordinator
- The broker that manages a consumer group's membership and stores its offsets.
- Heartbeat
- A periodic message from a group member to the coordinator proving it is alive.
- High watermark (HW)
- The highest offset copied to all in-sync replicas. Consumers read only below it.
- Hot partition
- A partition receiving much more traffic than others, usually because of a dominant key.
- Idempotent consumer
- A consumer whose processing gives the same result when a record is processed more than once.
- Idempotent producer
- A producer that tags batches with a producer ID and sequence numbers, so brokers drop duplicates from retries. On by default.
- In-sync replica set (ISR)
- The leader plus followers that are caught up within
replica.lag.time.max.ms. - Isolation level
- Consumer setting:
read_uncommitted(default) orread_committed, which hides aborted and open transactional records. - Key
- Optional bytes attached to a record. Determines the partition and therefore ordering.
- KIP
- Kafka Improvement Proposal: the design document behind each change, such as KIP-848 (new consumer protocol) or KIP-932 (share groups).
- KRaft
- Kafka's Raft-based metadata mode. The only mode since Kafka 4.0.
- KStream
- In Kafka Streams, an unbounded stream of independent events.
- KTable
- In Kafka Streams, a table of the latest value per key, updated by each record.
- Last stable offset (LSO)
- The first offset that belongs to a still-open transaction.
read_committedconsumers read up to it. - Leader
- The replica that handles produce requests (and usually fetches) for a partition.
- Leader election
- Choosing a new partition leader, normally from the ISR, when the current one fails, or to move leadership back to the preferred replica.
- Linger
linger.ms: how long a producer waits to fill a batch. 5 ms by default since Kafka 4.0.- Listener
- A broker endpoint (host, port, protocol). Advertised listeners are the addresses brokers tell clients to use.
- Log
- An ordered, append-only sequence of records. Each partition is a log.
- Log end offset (LEO)
- The offset the next appended record will get on a replica.
- Log segment
- One file of a partition's log, with its offset and time indexes. Retention deletes whole segments.
- Max poll interval
max.poll.interval.ms(5 min): the longest allowed gap betweenpoll()calls before a member is removed from its group.- Metadata
- Information about topics, partitions, leaders, and brokers that clients fetch to route requests.
- min.insync.replicas
- The minimum ISR size for a partition to accept
acks=allwrites. - MirrorMaker 2
- A Connect-based tool that replicates topics and consumer offsets between clusters.
- murmur2
- The hash the Java client's default partitioner applies to key bytes.
- Offset
- A record's position in a partition, assigned by the leader on append.
- Outbox pattern
- Writing an event row in the same database transaction as a business change, then publishing it with a relay such as CDC.
- Page cache
- Operating system memory caching file data. Kafka relies on it for fast reads and writes.
- Partition
- An ordered log that is a shard of a topic. The unit of parallelism, ordering, and replication.
- Partitioner
- Producer logic that chooses a partition for each record.
- Poison record
- A record that fails processing every time, blocking its partition if not handled.
- Poll loop
- The consumer's main loop:
poll(), process records, commit, repeat. - Preferred leader
- The first replica in a partition's assignment; leadership returns to it to balance load.
- Producer
- A client that writes records to topics.
- Producer ID (PID)
- An identifier the broker assigns to an idempotent or transactional producer.
- Rack awareness
- Placing a partition's replicas in different racks or zones so one failure does not take them all.
- Read committed
- See isolation level.
- Reassignment
- Moving partition replicas between brokers, for example onto new brokers.
- Rebalance
- Redistributing partitions among group members when membership or subscriptions change.
- Record
- The unit of data in Kafka: key, value, headers, timestamp, and (once stored) an offset.
- Replication factor (RF)
- The number of copies of each partition.
- Retention
- How long or how much data a topic keeps before deleting old segments.
- Retry topic
- A topic used to retry failed records later without blocking the main topic, at the cost of ordering.
- Schema registry
- A service that stores versioned schemas and checks compatibility; records carry a schema ID.
- Segment
- See log segment.
- Sequence number
- A per-partition counter on idempotent producer batches used to detect duplicates and gaps.
- Serializer
- Code that turns a key or value into bytes (deserializer: the reverse).
- Session timeout
- How long a member may go without heartbeats before the coordinator removes it (45 s).
- A group type (production-ready in Kafka 4.2) where members receive individual records with locks and acknowledgements, allowing more consumers than partitions and no per-key ordering.
- Sink connector
- A connector that writes from Kafka to an external system.
- Source connector
- A connector that reads from an external system into Kafka.
- State store
- Local storage (RocksDB or in-memory) for a Kafka Streams task, backed by a changelog topic.
- Static membership
- Giving a consumer a
group.instance.idso restarts within the session timeout do not trigger rebalances. - Sticky partitioner
- The keyless partitioning behavior: stay on one partition until a batch fills, then switch.
- Task
- In Kafka Streams and Connect, the unit of work assigned to a thread or worker, tied to input partitions.
- Tiered storage
- Moving older log segments to remote object storage while recent data stays on local disks.
- Tombstone
- A record with a key and a null value; in a compacted topic it marks the key as deleted.
- Topic
- A named stream of records, split into partitions.
- Transaction
- A set of writes (and consumer offsets) across partitions that commit or abort together.
- Transaction coordinator
- The broker that manages a transactional producer's state and writes commit or abort markers.
- __transaction_state
- The internal topic where transaction coordinators store transaction state.
- Transactional ID
- A stable name for a transactional producer, used to fence older instances and finish their transactions.
- Unclean leader election
- Electing an out-of-sync replica when no in-sync one is available. Restores availability, loses data. Off by default.
- Window
- A time range for aggregations: tumbling (fixed, no overlap), hopping (fixed, overlapping), sliding, or session (gap-based).
- Zero-copy
- Sending file data from the page cache to a socket without copying it through the application (
sendfile). Not possible with TLS. - ZooKeeper
- The external coordination service Kafka used for metadata before KRaft. Removed in Kafka 4.0.
Roadmap and revision
An 8-week plan at about one hour on weekdays plus one longer weekend session for labs and case studies. Stretch it to 12 weeks if your weekday time is shorter; the order matters more than the pace.
A daily session
A fixed slot of 45 to 60 minutes works well:
| Minutes | Do |
|---|---|
| 5 to 10 | Clear the revision queue on the home page, or redo one quiz you missed last time |
| 30 to 40 | One or two lessons: "Try it first", read, step through the figure, say the idea out loud |
| 5 to 10 | Tick the lesson studied; at the end of a module, do its self-check quiz |
Keep labs and case studies for the longer weekly session, where you have time to break things and design without rushing.
Weekly plan
| Week | Weekdays (about 1 hour each) | Weekend (about 2 hours) |
|---|---|---|
| 1 | M1, M2, M3 | Lab setup; labs for M2 and M3 |
| 2 | M4, M5, M6 | Labs for M4 to M6; play with the partition explorer |
| 3 | M7, M8, M9 | Labs for M7 to M9 |
| 4 | M10, M11, M12 | Labs for M10 to M12; case study 1 |
| 5 | M13, M14, M15 | Labs for M13 to M15; case study 2 |
| 6 | M16, M17, M18 | Labs for M16 to M18; case study 3; first mock |
| 7 | M19, M20, M21 | Labs for M19 and M20; incident drills in M21; case studies 4 and 5 |
| 8 | Case studies 6, 7, 8; interview bank | Case studies 9 and 10; readiness check |
After week 8: a 50-minute mock, then the revision queue every session, one practice prompt from the tracker a week, and one lab redone from memory a week.
Revision rules
- Spaced review: set a status after every case study and practice prompt. The queue brings designs back after 1, 2, 4, 10, or 30 days.
- Redesign, do not reread: on review, close the page, set a timer, and draw the design from scratch. Then compare.
- Redo labs from memory: once a week, run a past lab without reading its steps. If you cannot, the module is not done.
- One-line misses: keep a running list of what you forgot ("no key named", "forgot rebalance duplicates"). Read it before every mock.
- Mastery checklists: revisit them at the end of each phase; untick anything that is no longer true.
Mock interviews
| When | Format | Focus |
|---|---|---|
| Week 6 | 40 minutes with a peer, a Starter case study you have not seen | Five points for every Kafka box: key, partitions, delivery, failures, contract |
| Week 9 | 50 minutes, a Stretch prompt | Deep dives, failure handling, trade-offs at senior depth |
| Then | Every 1 to 2 weeks | Rotate practice prompts; include one "tell me about your Kafka system" round |
Using the handbook at work
- Before creating a topic: walk the five points on the interview page and size it with the partition calculator.
- Reviewing producer or consumer code: compare settings with the cheat sheets, and use each module's "Common mistakes" box as review questions.
- On call: follow the method in Module 21 and the "What happens when" table on the cheat sheets.
- Upgrading Kafka or Spring for Apache Kafka: search the handbook for "Version note" boxes; they list behavior that changed in recent releases.
Design practice tracker
All 10 case studies and 16 more practice prompts in one list. Set a status after each attempt; due designs come back in the revision queue on the home page.
Your designs
How to use practice prompts
Practice prompts have no write-up of their own. Each combines ideas from the case studies and modules listed under it. Attempt it with a 40-minute timer: requirements, estimates, topics and keys, the diagram, then the two hardest parts. Then reread the linked material to check your design, and set your status honestly.
| Status | When to choose it |
|---|---|
| Could not design it | You could not get a coherent design without help |
| Designed with hints | You needed to look something up for the core design |
| Designed independently | Full design, estimates, and two deep dives on your own |
| Needs revision | You did it before, but a redo went badly |
| Can teach it | Done twice, weeks apart, and you can answer every follow-up |
For every prompt, check your design against the five points: key, partitions, delivery guarantee, failure handling, and contract and retention.
Backup
Back up or move your progress
Progress saves to your account automatically. Use this only to move progress by hand, for example from a private window.
Back up or move your progress
Progress saves to your account automatically. Use this only to move progress by hand, for example from a private window.
SDE-2 Kafka readiness check
Tick an item only when you can do it with notes closed. When every box is ticked, you can build, run, and debug Kafka systems at SDE-2 level and handle Kafka questions in interviews.