Spring Kafka
Message Listener
Stateful Implementation
Java Programming
Kafka Tutorial

How to implement a stateful message listener using Spring Kafka?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Stateful message listeners in Spring Kafka are essential when maintaining the state of messages processed by a Kafka consumer is required. This can be useful in a variety of scenarios, such as tracking message offsets, managing session data, or implementing complex business logic that depends on previous messages. Below, we delve into effectively setting up and using a stateful message listener using the Spring Kafka library.

Understanding Spring Kafka

Spring Kafka abstracts much of the complexity of using Kafka and provides a higher-level, easier-to-use API for sending and receiving messages. It integrates seamlessly with Spring's messaging and event-driven programming model.

Key Components

To set up a stateful listener, you should familiarize yourself with these key Spring Kafka components:

  • Listener Container: Manages a set of message listener containers that listen to various topics.
  • KafkaTemplate: Simplifies sending messages to Kafka topics.
  • @KafkaListener Annotation: Marks a method to be the target of a Kafka message listener on specified topics.

Implementing a Stateful Message Listener

Implementing a stateful listener requires maintaining state across invocations of the listener method. Below are the steps to create a stateful message listener:

  1. Listener Configuration: Configure your Kafka listener with @EnableKafka and @Configuration classes.
java
1@EnableKafka
2@Configuration
3public class KafkaConfiguration {
4    @Bean
5    public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaListenerContainerFactory(ConsumerFactory<Object, Object> consumerFactory) {
6        ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
7        factory.setConsumerFactory(consumerFactory);
8        return factory;
9    }
10}
  1. Stateful Listener Implementation: Use a scoped-proxy mode if your listener needs to be stateful.
java
1@Component
2@Scope(scopeName = "thread", proxyMode = ScopedProxyMode.TARGET_CLASS)
3public class StatefulListener {
4
5    private int messageCount = 0;
6
7    @KafkaListener(topics = "your_topic_name", groupId = "group_id")
8    public void listen(String message) {
9        this.messageCount++;
10        processMessage(message);
11        // Optionally store state in a database or external cache
12    }
13
14    private void processMessage(String message) {
15        // Processing logic here
16        System.out.println("Processed message: " + message + "; Count: " + messageCount);
17    }
18}

By defining the @Scope as thread, each thread maintains its own instance of the StatefulListener, thereby preserving state across multiple invocations of listen().

Handling Offsets and Manual Committing

To precisely manage how message offsets are saved to ensure no message loss or duplication across restarts, manual offset management can be utilized.

java
1@Bean
2public ConcurrentKafkaListenerContainerFactory<?, ?> kafkaManualAckListenerContainerFactory(ConsumerFactory<Object, Object> consumerFactory) {
3    ConcurrentKafkaListenerContainerFactory<Object, Object> factory = new ConcurrentKafkaListenerContainerFactory<>();
4    factory.setConsumerFactory(consumerFactory);
5    factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
6    return factory;
7}
8
9@KafkaListener(topics = "your_topic_name", groupId = "group_id", containerFactory = "kafkaManualAckListenerContainerFactory")
10public void listenWithManualCommit(ConsumerRecord<?, ?> record, Acknowledgment acknowledgment) {
11    processRecord(record);
12    acknowledgment.acknowledge();
13}

Summary

Here's a table summarizing the key components for setting up a stateful message listener:

ComponentPurpose
@EnableKafkaEnables Kafka support.
@KafkaListenerDefines the method as a Kafka listener.
ConcurrentKafkaListenerContainerFactoryProvides the necessary configuration for Kafka listeners.
@ScopeDefines the scope (e.g., singleton, thread) to manage stateful behavior.

Additional Tips

  • Error Handling: Implement error handling within your listeners to manage recoverable situations and maintain state integrity.
  • Scalability Concerns: Address how your stateful listener scales, particularly around concerns like distributed caching if running multiple instances.
  • Testing: Test your listeners thoroughly, considering both state management and concurrency issues.

Implementing stateful listeners with Spring Kafka can enhance your application's capabilities, offering robust message handling tailored to your needs.


Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design