Spring Kafka Interview Questions
Spring for Apache Kafka layers a familiar template-and-listener programming model over the raw Kafka clients, so interviewers want to know that you understand both the abstraction and the machinery it hides. The strongest answers connect Spring annotations back to the underlying consumer poll loop, offset commits, and container threads — because that is where production bugs live. This page rehearses the questions most commonly asked about KafkaTemplate, @KafkaListener, error handling, retries, transactions, and testing. Each answer is concise enough to say out loud but precise enough to satisfy a senior reviewer.
Producers and KafkaTemplate
What does KafkaTemplate add over the raw KafkaProducer?
KafkaTemplate wraps a thread-safe KafkaProducer and gives you Spring conveniences: message conversion via a RecordMessageConverter, a fluent send(...) returning a CompletableFuture<SendResult>, automatic header propagation, observation/metrics integration, and participation in Spring transactions. You still configure the same underlying producer properties; the template just removes boilerplate and integrates with the rest of Spring.
@Service
public class OrderPublisher {
private final KafkaTemplate<String, OrderEvent> template;
public OrderPublisher(KafkaTemplate<String, OrderEvent> template) {
this.template = template;
}
public CompletableFuture<SendResult<String, OrderEvent>> publish(OrderEvent event) {
return template.send("orders", event.orderId(), event);
}
}
Is KafkaTemplate.send blocking?
No. send returns a CompletableFuture that completes when the broker acknowledges. The actual network I/O happens on the producer’s background sender thread. If you need synchronous behavior (for example, to fail fast in a request thread) you call .get() on the future, but that blocks and should be used deliberately.
How do you send to a specific partition or with headers?
Use a ProducerRecord (or the overloaded send signatures). You can set the partition, timestamp, key, value, and custom headers explicitly.
ProducerRecord<String, OrderEvent> record =
new ProducerRecord<>("orders", 2, order.orderId(), order);
record.headers().add("source", "checkout".getBytes(StandardCharsets.UTF_8));
template.send(record);
Consumers and @KafkaListener
How does @KafkaListener work under the hood?
The @EnableKafka infrastructure scans for @KafkaListener methods and registers a MessageListenerContainer for each via the KafkaListenerContainerFactory. The container runs the consumer poll() loop on its own thread, deserializes records, converts them to your method’s argument types, and invokes the method. You write a plain method; Spring owns the poll loop.
@KafkaListener(topics = "orders", groupId = "billing")
public void onOrder(OrderEvent event,
@Header(KafkaHeaders.RECEIVED_PARTITION) int partition) {
process(event);
}
What is the difference between a record listener and a batch listener?
A record listener is invoked once per record; a batch listener receives a List<ConsumerRecord> (or List<T>) from a single poll(). Batch mode amortizes per-call overhead and is faster for bulk writes, but you must handle partial failures yourself. Enable it with factory.setBatchListener(true).
@KafkaListener(topics = "metrics", batch = "true")
public void onBatch(List<MetricEvent> events) {
repository.saveAll(events);
}
What does container concurrency control?
concurrency (on the annotation or factory) sets how many consumer threads — and therefore distinct KafkaConsumer instances — the container runs within the group. Effective parallelism is min(concurrency, partitions); threads beyond the partition count sit idle. It scales consumption within one application instance.
| Setting | Where | Effect |
|---|---|---|
concurrency | listener / factory | Number of consumer threads in this instance |
groupId | listener | Consumer group the threads join |
| partitions | topic | Hard ceiling on useful concurrency |
Tip: Concurrency creates multiple consumers in the same group, not multiple threads sharing one consumer. The
KafkaConsumeritself is not thread-safe, so never call it from your own threads.
Acknowledgements and offsets
What are the AckModes and which should you use?
AckMode controls when the container commits offsets. The default with Spring Boot’s container is BATCH. For precise control, use MANUAL or MANUAL_IMMEDIATE and inject an Acknowledgment.
| AckMode | When offsets commit |
|---|---|
RECORD | After each record’s listener returns |
BATCH | After all records from one poll are processed |
TIME | After ackTime elapses |
COUNT | After ackCount records |
MANUAL | When you call ack.acknowledge(), queued until next poll |
MANUAL_IMMEDIATE | When you call ack.acknowledge(), committed immediately |
@KafkaListener(topics = "orders", groupId = "billing")
public void onOrder(OrderEvent event, Acknowledgment ack) {
process(event);
ack.acknowledge(); // requires AckMode.MANUAL or MANUAL_IMMEDIATE
}
Why disable enable.auto.commit in Spring Kafka?
Spring sets enable.auto.commit=false by default and manages commits through the container’s AckMode. This ties offset commits to successful listener execution, giving reliable at-least-once delivery rather than committing on a timer regardless of processing outcome.
Error handling and retries
How does DefaultErrorHandler work?
DefaultErrorHandler is the container’s error handler for record listeners. It applies a BackOff to retry a failed record in place (re-delivering from the consumer’s in-memory buffer, no re-poll), and after retries are exhausted it invokes a recoverer — commonly a DeadLetterPublishingRecoverer that forwards the record to a DLT. You can classify exceptions as non-retryable so they go straight to the DLT.
@Bean
DefaultErrorHandler errorHandler(KafkaTemplate<Object, Object> template) {
var recoverer = new DeadLetterPublishingRecoverer(template);
var handler = new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3));
handler.addNotRetryableExceptions(IllegalArgumentException.class);
return handler;
}
What is @RetryableTopic and how does it differ from in-place retry?
@RetryableTopic implements non-blocking retries: failed records are forwarded to a sequence of retry topics (orders-retry-0, orders-retry-1, …) with increasing delays, then to a DLT. Unlike DefaultErrorHandler’s blocking back-off, retries do not stall the main partition, so a slow-to-recover record does not hold up healthy traffic.
@RetryableTopic(attempts = "4", backoff = @Backoff(delay = 2000, multiplier = 2.0))
@KafkaListener(topics = "orders", groupId = "billing")
public void onOrder(OrderEvent event) {
process(event);
}
@DltHandler
public void onDlt(OrderEvent event) {
log.error("Exhausted retries for {}", event.orderId());
}
Warning: Non-blocking retries reorder records relative to the original partition, since retried records are reprocessed later from a separate topic. Use them only where strict per-key ordering is not required.
Transactions and testing
How do you make a read-process-write loop transactional?
Configure a KafkaTransactionManager and set a transactional.id prefix. With a transactional KafkaTemplate, sends inside the listener and the consumer’s offset commit happen atomically, and downstream consumers reading with isolation.level=read_committed never see aborted output.
spring:
kafka:
producer:
transaction-id-prefix: order-tx-
consumer:
isolation-level: read_committed
How do you test Spring Kafka without a real broker?
Use @EmbeddedKafka from spring-kafka-test, which boots an in-process KRaft broker for the test. Combine it with @SpringBootTest, then use a real producer to publish and KafkaTestUtils (or an awaitility poll) to assert the listener consumed the record.
@SpringBootTest
@EmbeddedKafka(partitions = 1, topics = "orders")
class OrderListenerTest {
@Autowired KafkaTemplate<String, OrderEvent> template;
@Autowired OrderRepository repository;
@Test
void consumesOrder() {
template.send("orders", "o-1", new OrderEvent("o-1", 42));
await().atMost(Duration.ofSeconds(10))
.untilAsserted(() -> assertThat(repository.findById("o-1")).isPresent());
}
}
Best Practices
- Inject
KafkaTemplateand let Spring own the producer lifecycle; never create raw producers inside listeners. - Use
AckMode.MANUAL_IMMEDIATEonly when you need precise commit control; otherwise the defaultBATCHis efficient and safe. - Set
concurrencyno higher than the partition count and remember each thread is a separate consumer. - Centralize error handling in a
DefaultErrorHandlerwith aDeadLetterPublishingRecoverer, and classify non-retryable exceptions explicitly. - Reach for
@RetryableTopicwhen blocking retries would stall the partition, but accept the reordering trade-off. - Write integration tests with
@EmbeddedKafkaand assert asynchronously with awaitility rather than fixed sleeps.