← Home
Apache Kafka

Top 8 Senior Interview Questions

Architecture · Delivery Guarantees · Ordering · Rebalancing · Patterns

Questions

  1. Kafka architecture — brokers, topics, partitions, consumer groups
  2. Delivery guarantees — at-most-once, at-least-once, exactly-once
  3. Consumer group rebalancing — impact and mitigation
  4. Partition key selection — ordering and hot partitions
  5. Offset management — manual vs auto commit
  6. Outbox pattern — guaranteed event publishing
  7. Dead Letter Topics — handling poison pills
  8. Kafka Streams vs Consumer API
Q1 · Architecture
Explain Kafka's architecture — brokers, topics, partitions, consumer groups, replication.

Building blocks

  • Broker: a Kafka server. A cluster has multiple brokers for fault tolerance.
  • Topic: named log of events (e.g. order-events). Append-only, immutable.
  • Partition: ordered, immutable sequence within a topic. Unit of parallelism and ordering.
  • Offset: integer position of a message within a partition. Consumers track their own offset.
  • Consumer Group: set of consumers sharing a topic. Each partition is consumed by exactly one consumer in the group at a time.
  • Replication: each partition has one leader + N replicas. Leader handles reads/writes; replicas replicate for durability.

Partition parallelism

Topic: order-events  (4 partitions)

Consumer Group A (3 consumers):
  Consumer 1 → Partition 0, Partition 1
  Consumer 2 → Partition 2
  Consumer 3 → Partition 3

Consumer Group B (6 consumers):
  Consumers 1-4 → Partitions 0-3
  Consumers 5-6 → IDLE (more consumers than partitions = waste)
Key rule: Max useful consumers in a group = number of partitions. Adding more consumers than partitions gives you idle consumers.

Replication and durability

  • acks=all: producer waits for all in-sync replicas (ISR) to acknowledge. Strongest durability.
  • min.insync.replicas=2: at least 2 replicas must confirm. Prevents data loss if leader dies.
  • If leader dies, Kafka elects a new leader from ISR in seconds.
Likely follow-up: "What is ZooKeeper vs KRaft mode?" → ZooKeeper was Kafka's external metadata store (being phased out). KRaft (Kafka Raft, Kafka 3.3+) is self-managed metadata — no ZooKeeper dependency, simpler operations, faster leader election.
Q2 · Delivery Semantics
Explain at-most-once, at-least-once, and exactly-once delivery. Which do you use in production?

Three semantics

SemanticMechanismRisk
At-most-onceacks=0, commit before processingMessage loss
At-least-onceacks=all + retry, commit after processingDuplicates
Exactly-once (EOS)Idempotent producer + transactions~4% throughput overhead

Idempotent producer (EOS component 1)

props.put("enable.idempotence", "true");
// Kafka assigns each producer a PID + sequence number per partition.
// Broker deduplicates retried messages with the same sequence number.
// Enables safe retries without duplicates on the broker side.

Transactional API (EOS component 2)

producer.initTransactions();
try {
    producer.beginTransaction();
    producer.send(new ProducerRecord("debit-events", debitMsg));
    producer.send(new ProducerRecord("credit-events", creditMsg));
    producer.sendOffsetsToTransaction(offsets, groupMetadata); // atomic commit
    producer.commitTransaction();
} catch (Exception e) {
    producer.abortTransaction();
}

Consumer side: read_committed

props.put("isolation.level", "read_committed");
// Consumer only sees messages from committed transactions.
// Aborted messages are filtered out.

Pragmatic choice

At-least-once + idempotent consumers is the standard in most production systems. EOS adds complexity and latency — use it only for critical internal pipelines where deduplication logic is impractical (e.g., financial ledger updates).

Likely follow-up: "How do you make a consumer idempotent?" → Check if the event was already processed using its unique ID (store processed IDs in DB or Redis). If found → skip. If not → process + mark as done atomically.
Q3 · Consumer Groups
What is a consumer group rebalance? How do you minimise its impact?

What triggers a rebalance

  • Consumer joins or leaves the group.
  • Consumer crashes or fails to send heartbeats within session.timeout.ms.
  • Partition count changes.

During rebalance: all consumption pauses until partitions are reassigned. Can cause duplicate processing if offsets weren't committed before the rebalance.

Mitigation strategies

  • Cooperative Sticky Assignor: incremental rebalance — only revoked partitions move, others stay. Minimal disruption vs eager (stop-the-world) rebalance.
  • Static group membership (group.instance.id): consumer rejoining within session.timeout.ms gets its old partitions back without triggering a rebalance. Great for rolling restarts.
  • Keep processing fast: slow consumers miss heartbeats → timeout → rebalance. Offload slow work to a thread pool, don't block the poll loop.

Key config values

session.timeout.ms = 45000       # how long before consumer is considered dead
heartbeat.interval.ms = 3000     # must be < session.timeout / 3
max.poll.interval.ms = 300000    # max time between poll() calls before leaving group
partition.assignment.strategy = CooperativeStickyAssignor
Likely follow-up: "What happens if processing is slow and max.poll.interval.ms is exceeded?" → Consumer is removed from the group and a rebalance triggers, even though the process is alive. Increase max.poll.interval.ms or reduce max.poll.records to process fewer messages per poll.
Q4 · Ordering & Partitioning
How do you choose a partition key? What happens with a bad key?

How partitioning works

Partition = hash(key) % numPartitions. Messages with the same key always go to the same partition → ordering guaranteed within a key.

Good key vs bad key

KeyResult
customerIdAll events for one customer are ordered
orderIdAll events for one order are ordered
eventTypeFew distinct values → hot partitions
nullRound-robin — no ordering, even distribution
same key for allEverything goes to partition 0 — single consumer, no parallelism

Hot partition problem

// Bad: "VIP" customers generate 80% of traffic
// All their events go to partition 2 → Consumer 2 is overwhelmed
// Fix: add a suffix to distribute VIP customers across partitions:
String key = customerId + "-" + (System.currentTimeMillis() % 3);
// Trade-off: ordering only within the sub-key, not across all events for that customer

Retries and ordering

Producer retries can reorder messages if max.in.flight.requests.per.connection > 1. Fix: enable idempotent producer (enable.idempotence=true) — handles reordering safely with up to 5 in-flight requests.

Likely follow-up: "How do you guarantee global ordering across all partitions?" → You can't, with multiple partitions. For global ordering, use a single partition — but that limits throughput to one consumer. Design your system so ordering is only needed within a key, not globally.
Q5 · Offsets
Explain consumer offset management. Auto vs manual commit — which and when?

What is an offset

An integer per (consumer group, topic, partition) tracking how far the consumer has read. Stored in the __consumer_offsets internal topic.

Auto-commit — the risks

enable.auto.commit=true
auto.commit.interval.ms=5000  # commits every 5 seconds

// Risk 1: message committed before processing completes
//   → consumer dies mid-processing → message is "lost" (skipped on restart)
// Risk 2: message processed but commit fails
//   → consumer restarts → reprocesses (duplicate)

Manual commit — the right approach

while (true) {
    ConsumerRecords<String, Event> records = consumer.poll(Duration.ofMillis(100));
    for (ConsumerRecord<String, Event> record : records) {
        processEvent(record.value());   // process first
    }
    consumer.commitSync();              // then commit — blocks until confirmed
}

// commitAsync() — non-blocking, higher throughput, but may fail silently.
// Use commitAsync() in the loop, commitSync() on shutdown for safety.

Committing specific offsets

// Per-partition commit for fine-grained control:
Map<TopicPartition, OffsetAndMetadata> offsets = new HashMap<>();
offsets.put(new TopicPartition(record.topic(), record.partition()),
            new OffsetAndMetadata(record.offset() + 1));  // +1 = next to read
consumer.commitSync(offsets);
Likely follow-up: "What is consumer lag and how do you monitor it?" → Lag = latest produced offset − last committed offset. Growing lag means consumers can't keep up. Monitor with Kafka JMX metrics or tools like Burrow. Alert when lag exceeds your SLA processing window.
Q6 · Patterns
What is the Outbox Pattern? Why is it critical for reliable event publishing?

The problem

A service needs to update the database AND publish a Kafka event atomically. These are two different systems — there's no distributed transaction spanning both.

// BROKEN — dual write with a gap:
db.save(order);           // success
kafka.send(orderEvent);   // crash here → event never sent, DB updated
// Downstream services never know the order was placed.

The Outbox solution

// In one DB transaction:
@Transactional
public void placeOrder(Order order) {
    orderRepo.save(order);                       // save business data
    outboxRepo.save(new OutboxEvent(            // save event in same transaction
        "order-events", order.getId(), toJson(order)));
}

// Separate relay process (Debezium CDC or scheduled poller):
// Reads unprocessed outbox rows → publishes to Kafka → marks as sent

Two relay approaches

  • Debezium CDC: reads the database's transaction log (WAL). Near-real-time. No polling overhead. Complex setup.
  • Polling relay: scheduled job queries outbox table for unprocessed rows. Simpler. Small latency (polling interval). Risk of thundering herd if rows pile up.
Key insight: The Outbox guarantees at-least-once delivery. The relay may re-publish if it crashes after publishing but before marking as sent. Your consumers must be idempotent.
Likely follow-up: "How is this different from dual-write with a retry?" → Retry still has a window where the app is down and the event is never retried. Outbox persists the intent durably in the DB first — the relay is decoupled and will pick it up whenever it runs, even after restarts.
Q7 · Error Handling
What is a poison pill message? How do you handle it with Dead Letter Topics?

The problem

A poison pill is a message that always fails processing (malformed JSON, unexpected schema, bad data). The consumer retries, fails, retries again → partition consumption is blocked. No new messages from that partition are processed.

Dead Letter Topic (DLT) pattern

// Spring Kafka DefaultErrorHandler with DLT:
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<?, ?> template) {
    DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template,
        (record, ex) -> new TopicPartition(record.topic() + ".DLT", record.partition()));

    FixedBackOff backOff = new FixedBackOff(1000L, 3); // 3 retries, 1s apart
    return new DefaultErrorHandler(recoverer, backOff);
}

Retry topic chain pattern (Spring Kafka 2.7+)

order-events
  → order-events-retry-1  (delay: 5s)
  → order-events-retry-2  (delay: 30s)
  → order-events-retry-3  (delay: 5min)
  → order-events-DLT      (manual investigation)

Exponential backoff across separate retry topics. The main topic never slows down. DLT messages are monitored — alert ops, allow manual replay after fixing the root cause.

Never silently drop a failed message. In financial systems, every event has business significance. DLT + alerting is mandatory.
Likely follow-up: "How do you replay DLT messages after fixing a bug?" → Consume from the DLT topic and re-publish to the original topic. Tools: kafka-consumer-groups CLI to reset offsets, or a dedicated replay service. Ensure your consumer is idempotent before replaying.
Q8 · Stream Processing
Kafka Streams vs Consumer API — when do you use each?

Consumer API

  • Low-level, poll-based. You manage state, offsets, threading, error handling.
  • Best for: simple consume-transform-produce, forwarding events, calling external APIs per message.

Kafka Streams

  • Client library (runs in your app — no separate cluster). Built on Consumer API.
  • Provides: stateful operations (aggregations, joins), windowing, exactly-once, automatic offset management, state stores (RocksDB).
KStream<String, Order> orders = builder.stream("orders");

orders
    .filter((key, order) -> order.getAmount().compareTo(THRESHOLD) > 0)
    .groupByKey()
    .windowedBy(TimeWindows.ofSizeWithNoGrace(Duration.ofMinutes(5)))
    .count()                        // orders per customer per 5-min window
    .toStream()
    .filter((key, count) -> count > 10)  // suspicious activity
    .to("fraud-alerts");

Decision guide

Use caseChoose
Simple event forwarding / enrichmentConsumer API
Aggregations, counts, sums over time windowsKafka Streams
Joining two event streamsKafka Streams
Calling external REST API per messageConsumer API
Real-time fraud scoring (sliding windows)Kafka Streams
Likely follow-up: "How does Kafka Streams handle state store recovery after a crash?" → State is backed by a changelog topic in Kafka. On restart, the application replays the changelog to rebuild the RocksDB state store. With standby replicas (num.standby.replicas), failover is near-instant.