Consumer groups vs consumer instances, committed vs current offsets, reprocessing and replay, at-least-once/at-most-once/exactly-once, ISR and what happens when it shrinks or empties, tuning for throughput, idempotent producers, consumer crashes, handling duplicates with idempotent consumers, scaling Kafka, retention policies, Kafka Streams vs the consumer API and stream joins, back-pressure in consumers, and designing a booking event flow.
Published September 25, 2026
Kafka interviews at senior level focus on guarantees and their costs:
acks, the ISR, min.insync.replicas);Kafka 4.0 removed ZooKeeper (KRaft only), so mention that when you describe the architecture.
Short answer:
KafkaConsumer (one thread, or one listener container thread), which reads from the partitions assigned to it.group.id. Each partition is assigned to exactly one instance in the group, which gives load balancing and ordering per partition. The group has one committed offset per partition.CooperativeStickyAssignor), static membership (group.instance.id), and Kafka 4's new consumer rebalance protocol (KIP-848) to reduce the disruption.Learn it in depth → Messaging Technology Choices
Short answer:
poll().__consumer_offsets topic) for the group and partition. It's where the group resumes after a restart or rebalance. It's committed automatically (enable.auto.commit=true, every 5 seconds by default, which risks loss or duplicates) or manually (commitSync/commitAsync; in Spring Kafka, AckMode.RECORD/BATCH/MANUAL_IMMEDIATE).auto.offset.reset (earliest/latest) applies only when no committed offset exists, or it's out of range (expired by retention).Short answer: Kafka is a durable, replayable log: records stay for the retention period, regardless of consumption. To replay:
kafka-consumer-groups.sh --reset-offsets --to-datetime/--to-earliest/--shift-by --execute (with the group stopped), or programmatically with seek/seekToBeginning/offsetsForTimes. Spring Kafka offers ConsumerSeekAware;Design for replay: idempotent consumers (replays produce duplicates), and deterministic processing (avoid side effects like emails on replay, or guard them). Version the event schemas.
Short answer:
acks=0, no retries). Messages can be lost, never duplicated. It's acceptable for lossy telemetry.acks=all plus retries, and consumers commit after processing. There's no loss, but duplicates are possible (producer retries, consumer crashes before committing, rebalances). This is the common default, made safe by idempotent consumers.transactional.id; atomic writes to several partitions, plus committing the consumer offsets in the same transaction through sendOffsetsToTransaction), plus consumers reading with isolation.level=read_committed. Kafka Streams enables it with processing.guarantee=exactly_once_v2;Learn it in depth → Event-Driven Architecture Patterns
Short answer: Each partition has a leader, and followers that replicate its log. The ISR is the set of replicas fully caught up with the leader (within replica.lag.time.max.ms).
acks=all, a write is acknowledged only when all the ISR members have it, and min.insync.replicas (for example 2, with replication factor 3) sets the minimum ISR size required to accept writes.min.insync.replicas, producers with acks=all get NotEnoughReplicasException: writes are rejected, in favour of durability. Reads continue from the leader.unclean.leader.election.enable=true) lets an out-of-sync replica become the leader. That restores availability, but loses data. It's off by default. That's the classic C-versus-A choice.min.insync.replicas=2, acks=all, idempotent producers, and unclean election disabled.Short answer:
batch.size (for example 64–256 KB) and linger.ms (5–20 ms), trading a little latency for much bigger batches;compression.type=zstd or lz4;buffer.memory;max.in.flight.requests.per.connection ≤ 5, with idempotence (which preserves ordering);get() per message.fetch.min.bytes/fetch.max.wait.ms for bigger fetches;max.poll.records;max.poll.interval.ms.kafka-producer-perf-test/kafka-consumer-perf-test, and watch the broker request latency, under-replicated partitions, and consumer lag.Short answer: With enable.idempotence=true (the default since Kafka 3.0), the broker assigns the producer a producer ID (PID), and the producer attaches a sequence number per partition to each batch. The broker de-duplicates retried batches (the same PID and sequence), and rejects out-of-order sequences. So retries can't create duplicates or reorder records within a partition, for a single producer session. It requires acks=all, retries > 0, and max.in.flight.requests.per.connection ≤ 5. Its limits: it covers only a single producer session and one partition. Across restarts, or several partitions atomically, you need transactions (transactional.id). It doesn't de-duplicate application-level re-sends of the same business event, so include an event ID for consumers.
Short answer:
session.timeout.ms (45 seconds by default) the group coordinator removes it. If processing hangs without polling, max.poll.interval.ms expiry kicks it out.group.instance.id), a quick restart within the session timeout avoids a rebalance entirely.Handle it: commit after processing, keep processing short, store external side effects idempotently, and monitor rebalance rates and lag.
Short answer: Assume at-least-once delivery, and make processing idempotent:
processed_events(event_id PRIMARY KEY, processed_at) table, inserted in the same database transaction as the business change. On a duplicate key, skip. This is the inbox pattern. Keep a TTL, or clean up old IDs.UPDATE order SET status='SHIPPED' WHERE id=?) instead of increments, or conditional updates using versions or sequence numbers (apply only if event.version > stored.version).SET NX with a TTL) for cheap deduplication, if a small risk of loss on eviction is acceptable.@KafkaListener(topics = "payments.completed", groupId = "orders")
@Transactional
public void on(PaymentCompleted e) {
if (inbox.alreadyProcessed(e.eventId())) return; // INSERT … ON CONFLICT DO NOTHING returns 0 rows
orders.markPaid(e.orderId(), e.paymentRef());
}
Short answer:
kafka-reassign-partitions.sh, Cruise Control) to spread the load. Scale disks (tiered storage, KIP-405, offloads old segments to object storage). Scale the network.Short answer: Retention decides how long records stay in a topic, independent of consumption:
retention.ms (7 days by default);retention.bytes per partition;segment.ms/segment.bytes), so data can outlive the limit until its segment rolls.cleanup.policy=delete (the default) drops old segments. compact keeps the latest record per key (plus tombstones for deletes, for delete.retention.ms), for changelogs and state. compact,delete combines both.Short answer:
Kafka Streams: a client library (not a cluster) for stateful stream processing, with a DSL (KStream, KTable, GlobalKTable): filter, map, groupBy, aggregations, windowing (tumbling, hopping, session), joins, and exactly-once processing. It keeps local state stores (RocksDB) backed by changelog topics (fault-tolerant, restorable), and scales by running more instances (tasks per partition). Interactive queries expose the state.
The plain consumer API: you poll records and handle everything yourself: state, windowing, fault tolerance, rebalancing of state, and EOS. It's fine for simple per-record processing (and Spring's @KafkaListener).
Joins:
Stream–stream and stream–table joins require co-partitioning: the same number of partitions, and the same key, so repartition (selectKey + repartition) if needed.
KStream<String, Order> orders = builder.stream("orders");
KTable<String, Customer> customers = builder.table("customers");
orders.selectKey((k, o) -> o.customerId())
.join(customers, (order, customer) -> new EnrichedOrder(order, customer.tier()))
.to("orders.enriched");
Short answer: Kafka consumers are pull-based, so they naturally consume only as fast as they poll. The real risks are processing slower than the poll deadline, and unbounded in-memory buffering:
max.poll.records and processing time to stay within max.poll.interval.ms, or you get evicted, then rebalance, then reprocess.consumer.pause(...), or Spring Kafka's container pause()/resume()) when downstream systems are slow or overloaded (a full database pool, an open circuit breaker), while still polling to keep the group membership.Short answer:
BookingRequested/BookingHeld to an outbox in the same transaction. Debezium or a relay publishes it to bookings.events (keyed by booking ID, for per-booking ordering).BookingHeld, charges with an idempotency key, and emits PaymentCompleted or PaymentFailed. The Booking service consumes those, and confirms (BookingConfirmed) or releases the hold (BookingCancelled, a compensation). Hold expiry through a scheduler or delayed topic cancels unpaid holds.BookingConfirmed v2), and an event ID plus correlation ID in the headers.Q: How is ordering guaranteed in Kafka, and when is it lost?
A: Only within a partition. The producer key determines the partition, so events for one entity stay ordered. It's lost if you change the partition count, use no key or random keys, retry without idempotence and with max.in.flight > 1 (old versions), or process one partition's records concurrently in the consumer.
Q: What did KRaft change? A: Kafka stores its metadata in an internal Raft-based quorum of controllers, instead of ZooKeeper. It gives simpler operations, faster controller failover, and support for far more partitions. Kafka 4.0 removed ZooKeeper mode entirely.
Q: What is a tombstone?
A: A record with a key and a null value. In compacted topics, it marks the key for deletion (it's removed after delete.retention.ms), and sink connectors use it to delete the downstream rows.
Q: How many partitions should a topic have? A: Enough to meet the peak throughput (target MB/s divided by per-partition consumer throughput), with headroom for consumer parallelism. Avoid huge counts without need: they cost broker memory and file handles, and lengthen failover. It's easier to over-provision moderately at creation than to repartition later.