Intermediate · Decouple
Design Retries, Ordering & Dead Letters
Classify transient and permanent failures without building an invisible infinite retry loop.
Primary source: Spring Boot 3.5 Reference: Messaging with Apache Kafka
Lesson 3 of 4
1d streakThe Architectural Question
Retrying poison messages forever buries the partition; dropping them loses money—failure classification and dead-letter ownership are the actual design.
What this guide proves
- Use bounded exponential backoff with jitter
- Preserve ordering only where the domain requires it
- Treat dead letters as an operational workflow with ownership
Error Handling in Kafka: DefaultErrorHandler & Exponential Backoff
When an exception occurs during message consumption in a @KafkaListener, Spring Kafka relies on a CommonErrorHandler. In modern Spring Kafka 3.x / Spring Boot 3.5, DefaultErrorHandler replaces deprecated SeekToCurrentErrorHandler.
DefaultErrorHandler allows configuring retry backoff, max delivery attempts, and classifying fatal versus non-fatal exceptions.
package com.example.kafka;
import org.apache.kafka.common.errors.SerializationException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.ExponentialBackOff;
@Configuration(proxyBeanMethods = false)
public class KafkaErrorConfig {
@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> kafkaTemplate) {
// Exponential backoff: 500ms, 1000ms, 2000ms (multiplier 2.0, max 3 attempts)
ExponentialBackOff backOff = new ExponentialBackOff(500L, 2.0);
backOff.setMaxElapsedTime(5000L);
// DeadLetterPublishingRecoverer routes failed messages to topic.DLT
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate);
DefaultErrorHandler handler = new DefaultErrorHandler(recoverer, backOff);
// Classify non-retryable fatal exceptions (fail fast to DLT immediately!)
handler.addNotRetryableExceptions(
SerializationException.class,
IllegalArgumentException.class,
ClassCastException.class
);
return handler;
}
}Fatal vs Non-Fatal Exceptions
Dead Letter Queues (DLT) & Header Propagation
When all retry attempts are exhausted, DeadLetterPublishingRecoverer publishes the failed message to a designated Dead Letter Topic (DLT), typically named <original-topic>.DLT. It automatically attaches debugging headers:
| Kafka Header | Description | Value Example |
|---|---|---|
| kafka_dlt-original-topic | Source topic where failure occurred | orders.placed.v1 |
| kafka_dlt-original-partition | Source partition number | 2 |
| kafka_dlt-original-offset | Source offset of failed message | 10482 |
| kafka_dlt-exception-message | Exception message string | Connection refused to payment gateway |
| kafka_dlt-exception-stacktrace | Full Java exception stacktrace | java.net.ConnectException: ... |
Dead Letter Reprocessing Strategies & Operational Remediation
A dead letter queue is not a trash can; it is an active operational backlog. In production, provide a dedicated consumer or operational tool to reprocess resolved messages after fixing downstream bugs or database issues.
The Secondary DLQ Poison Loop
Simulating Poison Pills and Verifying DLT Routing
Verify retry backoff and dead letter publishing by injecting transient and permanent exceptions:
# 1. Produce malformed message (poison pill) to Kafka topic
kcat -b localhost:9092 -t orders.placed.v1 -P <<EOF
{"corrupt_json": true
EOF
# 2. Observe consumer log:
# WARN o.s.k.l.DefaultErrorHandler - Fatal SerializationException: Sending to DLT immediately
# 3. Consume from Dead Letter Topic to verify error headers and message body
kcat -b localhost:9092 -t orders.placed.v1.DLT -C -e -u -J | jq .Active Recall Checkpoint
Why should deserialization errors never be retried with backoff?
Hint: Differentiate transient network errors from permanent data corruption.
Show answer & explanation
A deserialization error is caused by invalid syntax or schema incompatibility. Re-reading the identical malformed byte payload will produce the same error every time, wasting CPU and blocking the partition.
Runtime Architecture & Flow
Reliable event flow · runtime visualization
Design Retries, Ordering & Dead Letters
Local commit
Use bounded exponential backoff with jitter
Publisher
Preserve ordering only where the domain requires it
Broker
Treat dead letters as an operational workflow with ownership
Consumer
Route poison messages to a dead-letter topic and build a replay procedure.
Working Implementation
Code Walkthrough
Route poison messages to a dead-letter topic and build a replay procedure.
package com.example.kafka;
import org.apache.kafka.common.errors.SerializationException;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
import org.springframework.kafka.listener.DefaultErrorHandler;
import org.springframework.util.backoff.ExponentialBackOff;
public final class KafkaDlqRetryLab {
private KafkaDlqRetryLab() {}
public static DefaultErrorHandler buildResilientErrorHandler(KafkaTemplate<String, Object> template) {
// Backoff: 500ms initial, 2.0 multiplier, max 3 attempts
var backoff = new ExponentialBackOff(500L, 2.0);
backoff.setMaxElapsedTime(5000L);
var recoverer = new DeadLetterPublishingRecoverer(template);
var handler = new DefaultErrorHandler(recoverer, backoff);
// Fail poison pills to DLT immediately without wasting retry cycles
handler.addNotRetryableExceptions(
SerializationException.class,
IllegalArgumentException.class,
ClassCastException.class
);
return handler;
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Classify transient versus permanent failures in one policy object
- Back off exponentially with jitter and cap total attempts
- Operate dead letters as owned work with triage and replay tooling
Key Takeaway
Kafka error handling requires DefaultErrorHandler with exponential backoff for transient network errors and immediate DLT routing for fatal serialization poison pills.
Architectural & Technical Review
Core Interview Questions
Q01Why should DeserializationException and malformed JSON payloads never be retried with backoff?Show answer
Deserialization errors are permanent data corruption bugs (poison pills). Re-evaluating the identical malformed byte payload will never succeed, wasting CPU and blocking partition consumption. Fatal exceptions must route to the DLT immediately.
Q02What metadata headers does DeadLetterPublishingRecoverer attach when routing a message to the DLT?Show answer
It attaches original topic name, original partition number, original message offset, exception message, and the full Java exception stack trace as Kafka record headers, providing full diagnostic context for operational triage.
Q03What operational safeguard is required when building a DLT reprocessing consumer?Show answer
The DLT consumer must catch and log terminal errors to prevent infinite poison pill retry loops that would continuously bounce failing messages back into the dead letter topic.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Retrying non-retryable poison pills with infinite backoff in Kafka listeners — Permanently stalls consumer partition progress, causing consumer lag to skyrocket across the cluster.
- Pitfall: Discarding failed messages silently without publishing to a Dead Letter Topic — Destroys transaction auditability and loses customer orders without any operational alert.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources