java
44 lines · 7 steps
Kafka retry and dead-letter handling in Spring
A Spring config that retries failing Kafka records with backoff, then routes the incurable ones to a dead-letter topic.
Explained by
highlit
1@Configuration
2@EnableKafka
3public class KafkaErrorHandlingConfig {
4
5 @Bean
6 public DeadLetterPublishingRecoverer deadLetterRecoverer(KafkaTemplate<Object, Object> template) {
7 return new DeadLetterPublishingRecoverer(template,
8 (record, exception) -> new TopicPartition(record.topic() + ".DLT", record.partition()));
9 }
10
11 @Bean
12 public DefaultErrorHandler errorHandler(DeadLetterPublishingRecoverer recoverer) {
13 ExponentialBackOffWithMaxRetries backOff = new ExponentialBackOffWithMaxRetries(4);
14 backOff.setInitialInterval(500L);
15 backOff.setMultiplier(2.0);
16 backOff.setMaxInterval(10_000L);
17
18 DefaultErrorHandler handler = new DefaultErrorHandler(recoverer, backOff);
19 handler.addNotRetryableExceptions(
20 DeserializationException.class,
21 MethodArgumentResolutionException.class,
22 IllegalArgumentException.class);
23 handler.setRetryListeners((record, ex, attempt) ->
24 log.warn("Retry {} for record from {}-{}@{}: {}",
25 attempt, record.topic(), record.partition(), record.offset(), ex.getMessage()));
26 return handler;
27 }
28
29 @Bean
30 public ConcurrentKafkaListenerContainerFactory<String, OrderEvent> kafkaListenerContainerFactory(
31 ConsumerFactory<String, OrderEvent> consumerFactory,
32 DefaultErrorHandler errorHandler) {
33
34 ConcurrentKafkaListenerContainerFactory<String, OrderEvent> factory =
35 new ConcurrentKafkaListenerContainerFactory<>();
36 factory.setConsumerFactory(consumerFactory);
37 factory.setCommonErrorHandler(errorHandler);
38 factory.setConcurrency(3);
39 factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.RECORD);
40 return factory;
41 }
42
43 private static final Logger log = LoggerFactory.getLogger(KafkaErrorHandlingConfig.class);
44}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Separating retryable from non-retryable exceptions stops you from wasting attempts on errors that can never succeed.
- 2Exponential backoff spaces out retries so a struggling downstream isn't hammered while it recovers.
- 3A dead-letter recoverer gives permanently failing records a home instead of blocking the partition forever.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/kafka-retry-and-dead-letter-handling-in-spring-explained-java-9e0b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.