Spring for Apache Kafka: Producer and Consumer Patterns
Introduction
In the world of microservices, event-driven architecture has become a cornerstone for building scalable, decoupled systems. At the heart of this paradigm lies Apache Kafka—a distributed event streaming platform that handles trillions of events a day. But raw Kafka APIs can be verbose and error-prone. That’s where Spring for Apache Kafka comes in, offering a robust abstraction layer that simplifies producer and consumer development while retaining the flexibility of the underlying Kafka client.
In this post, I’ll walk you through the essential producer and consumer patterns using Spring Boot. We’ll cover configuration, message serialization, error handling, retries, and advanced patterns like batch consumption and manual acknowledgments. Whether you’re new to Kafka or looking to refine your existing setup, this guide provides practical, battle-tested code snippets you can use immediately.
Why Spring for Apache Kafka?
Spring for Apache Kafka (spring-kafka) provides:
- Declarative configuration via
application.ymlorapplication.properties. - Templated operations with
KafkaTemplatefor sending messages. - Listener containers that manage consumer lifecycles and threading.
- Seamless integration with Spring Boot’s auto-configuration.
- Robust error handling with
ErrorHandlerandRetryTemplate.
Let’s dive into the core patterns.
Setting Up Dependencies
First, add the necessary dependencies to your pom.xml (Maven) or build.gradle (Gradle).
1 | <dependency> |
For Gradle:
1 | implementation 'org.springframework.kafka:spring-kafka' |
Producer Patterns
Basic Producer Configuration
In your application.yml, configure the producer properties:
1 | spring: |
enable.idempotenceensures exactly-once semantics for producers.acks=allguarantees that the leader and all in-sync replicas acknowledge the message.retrieshandles transient network errors.
Sending Messages with KafkaTemplate
Create a service that uses KafkaTemplate to send messages:
1 |
|
The send method takes the topic name, key, and value. The key determines partitioning; messages with the same key go to the same partition, preserving order.
Customizing the KafkaTemplate
You can customize the KafkaTemplate bean if needed. For example, to set a default topic or add headers:
1 |
|
Asynchronous Sends and Callbacks
By default, send is asynchronous. To handle results or errors, use a callback:
1 | ListenableFuture<SendResult<String, Order>> future = kafkaTemplate.send("orders", order); |
In newer Spring versions, you can use CompletableFuture:
1 | CompletableFuture<SendResult<String, Order>> future = kafkaTemplate.send("orders", order).completable(); |
Producer Interceptors and Custom Serializers
If you need to add custom headers or transform messages before sending, implement a ProducerInterceptor:
1 | public class TraceProducerInterceptor implements ProducerInterceptor<String, Order> { |
Register it in the producer properties:
1 | spring: |
Consumer Patterns
Basic Consumer Configuration
Configure consumer properties in application.yml:
1 | spring: |
group-iddefines the consumer group; multiple instances with the same group share the load.auto.offset.reset=earlieststarts reading from the beginning if no committed offset exists.enable.auto.commit=falsegives you manual control over offset commits, which we’ll discuss later.
Implementing a Consumer with @KafkaListener
The simplest way to consume messages is the @KafkaListener annotation:
1 |
|
You can access the message headers and partition info via ConsumerRecord:
1 |
|
Batch Consumption
For high-throughput scenarios, you can consume messages in batches:
1 |
|
You need to configure a listener container factory that supports batch mode:
1 |
|
Manual Acknowledgment
If you need to commit offsets only after successful processing, use manual acknowledgment:
1 |
|
Remember to set enable.auto.commit=false in your consumer config.
Error Handling and Retry
Spring Kafka provides several mechanisms for handling errors. The simplest is to use a DefaultErrorHandler with a FixedBackOff:
1 |
|
Then attach it to the container factory:
1 |
|
For more complex retry logic, you can use RetryTemplate:
1 |
|
Dead Letter Topic (DLT) Pattern
When messages fail after retries, it’s common to send them to a Dead Letter Topic for later inspection. Spring Kafka supports this via @RetryableTopic:
1 |
|
This automatically creates topics like orders-retry-0, orders-retry-1, etc., and finally orders-dlt. To consume from the DLT:
1 |
|
Consumer Seek and Idle Handling
If you need to rewind offsets (e.g., for reprocessing), you can use SeekToCurrentErrorHandler or manually seek:
1 |
|
For handling idle consumers (e.g., to send heartbeats), implement ConsumerSeekAware or use ContainerStoppingErrorHandler.
Advanced Patterns
Compacted Topics and Keyed Messages
For stateful processing, use compacted topics where only the latest value for each key is retained. Configure the topic with cleanup.policy=compact and ensure your producer uses meaningful keys.
Transactions
Spring Kafka supports transactions to ensure exactly-once semantics across producers and consumers. Enable transactions on the producer:
1 | spring: |
Then use @Transactional on your method:
1 |
|
Custom Message Converters
If you need to convert messages to a custom format (e.g., Avro), implement a MessageConverter:
1 |
|
Testing Kafka Applications
Spring Kafka provides @EmbeddedKafka for integration testing:
1 |
|
Monitoring and Observability
Use Spring Boot Actuator to expose Kafka metrics:
1 | management: |
You can also integrate with Micrometer to track consumer lag via Kafka’s KafkaConsumerMetrics.
Common Pitfalls and Best Practices
- Avoid heavy processing in the listener thread; offload to a separate executor if needed.
- Set appropriate
max.poll.recordsto control batch size and avoid long processing times. - Use
spring.kafka.consumer.properties.max.poll.interval.msto prevent consumer rebalancing during long processing. - Always close the
KafkaTemplatein a@PreDestroymethod if you create it manually. - Handle deserialization exceptions gracefully; use a custom
ErrorHandlerto avoid infinite loops. - Test with
@EmbeddedKafkato catch issues early.
Key Takeaways
- Spring for Apache Kafka abstracts the complexity of Kafka clients, allowing you to focus on business logic.
- Use
KafkaTemplatefor reliable, asynchronous message sending with optional callbacks. @KafkaListeneris the cornerstone for consumer development, supporting batch, manual acknowledgment, and error handling.- Implement retries with
DefaultErrorHandleror@RetryableTopicto build resilient consumers. - Leverage dead letter topics to handle poison messages without blocking the main flow.
- For exactly-once semantics, combine idempotent producers with transactional support.
- Always configure proper serializers, trusted packages, and offset management to avoid production surprises.
By mastering these patterns, you can build robust, scalable event-driven microservices with Spring Boot and Apache Kafka. Happy coding!