Event-Driven Architecture with Kafka and Spring Boot: A Practical Guide
Event-Driven Architecture with Kafka and Spring Boot: A Practical Guide
Event-driven architecture (EDA) has become a cornerstone of modern, scalable systems. By decoupling services and enabling asynchronous communication, EDA allows teams to build resilient, responsive applications that can handle massive throughput. Apache Kafka, combined with Spring Boot, provides a powerful yet approachable stack for implementing this pattern.
In this guide, I’ll walk through the core concepts of event-driven design, how to set up Kafka with Spring Boot, and share battle-tested patterns for producers, consumers, error handling, and schema management. Whether you’re new to Kafka or looking to refine your approach, this post will give you practical, production-ready knowledge.
Why Event-Driven Architecture?
Traditional synchronous communication (REST, gRPC) creates tight coupling between services. If Service A calls Service B and B is slow or down, A suffers. In an event-driven system, services communicate through events—immutable records of something that happened.
Key benefits:
- Decoupling: Producers and consumers don’t need to know about each other
- Scalability: Each component can scale independently based on its own load
- Resilience: Failures are isolated; events can be replayed
- Auditability: Event logs provide a complete history of state changes
Apache Kafka in a Nutshell
Kafka is a distributed event streaming platform. At its core:
- Topics: Categories for events (like database tables)
- Partitions: Subdivisions of topics for parallelism
- Producers: Publish events to topics
- Consumers: Subscribe to topics and process events
- Brokers: Servers that store and serve events
Kafka guarantees ordering within a partition and retains events even after consumption (configurable retention period). This makes it ideal for event sourcing, stream processing, and data pipelines.
Setting Up Spring Boot with Kafka
Spring Boot provides excellent support via spring-kafka. Let’s start with the basics.
Dependencies
Add the following to your pom.xml:
1 | <dependency> |
Configuration
In application.yml:
1 | spring: |
For local development, start Kafka using Docker:
1 | docker run -d --name kafka \ |
Building a Producer
A producer publishes events to a topic. Let’s create an order event producer.
Event Class
1 | public class OrderEvent { |
Producer Service
1 |
|
Key points:
- Use the event’s natural key (e.g.,
orderId) as the Kafka key to maintain ordering per entity - Handle async results properly—never ignore the future
- Log success and failure for observability
Building a Consumer
Consumers process events. Spring Kafka makes this trivial with @KafkaListener.
1 |
|
Consumer Configuration
For fine-grained control, define a ConcurrentKafkaListenerContainerFactory:
1 |
|
Error Handling and Retries
Failures happen. A consumer might throw an exception due to a database error or invalid data. How you handle this defines your system’s resilience.
Dead Letter Topic (DLT)
Spring Kafka supports automatic DLT handling:
1 |
|
This configuration will retry 3 times (total 4 attempts) with exponential backoff, then send the failed event to a -dlt topic.
Manual Acknowledgment
For more control, use manual acknowledgment:
1 |
|
Schema Management with Avro
As your system grows, event schemas evolve. Using Avro with Schema Registry provides compatibility guarantees.
Setup
Add dependencies:
1 | <dependency> |
Avro Schema
1 | { |
Producer with Avro
1 |
|
Schema Registry ensures that producers and consumers use compatible schemas, preventing runtime errors from schema drift.
Idempotent Consumers
In distributed systems, events can be delivered more than once (at-least-once semantics). Your consumers must be idempotent.
Pattern: Idempotency Key
1 |
|
This ensures that even if the same event is consumed twice, the side effects happen only once.
Testing Kafka Producers and Consumers
Testing asynchronous systems requires special care. Spring Kafka provides excellent test support.
Unit Testing a Producer
1 |
|
Integration Testing a Consumer
1 |
|
Monitoring and Observability
In production, you need to know what’s happening. Integrate with Micrometer and expose metrics.
Metrics Configuration
1 | spring: |
Spring Boot auto-configures Micrometer metrics for Kafka. Expose them via Actuator:
1 | curl localhost:8080/actuator/metrics/kafka.producer.record.send.total |
Distributed Tracing
Use Spring Cloud Sleuth to trace events across services:
1 | <dependency> |
Headers like X-B3-TraceId are automatically propagated through Kafka headers.
Production Best Practices
- Use compacted topics for state: If you need the latest state per key (e.g., customer profile), use log compaction.
- Partition count: Start with more partitions than consumers. You can increase later but never decrease.
- Replication factor: At least 3 for production to tolerate broker failures.
- Monitor consumer lag: Use tools like Burrow or Kafka Lag Exporter to detect slow consumers.
- Graceful shutdown: Implement
@PreDestroyto close producers and consumers cleanly. - Security: Enable SSL and SASL authentication in production.
Putting It All Together: A Real-World Example
Let’s model a simple order processing pipeline:
- Order Service publishes
OrderCreatedevent - Inventory Service consumes, reserves stock, publishes
InventoryReservedorInventoryFailed - Payment Service consumes
InventoryReserved, processes payment, publishesPaymentCompleted - Shipping Service consumes
PaymentCompleted, creates shipment
Each service is a separate Spring Boot application with its own Kafka producer/consumer. The event flow is asynchronous, resilient, and scalable.
1 | // Inventory Service Consumer |
This pattern allows each service to scale independently. If the inventory service is down, orders are still accepted—they’ll be processed when inventory comes back online.
Key Takeaways
- Event-driven architecture decouples services and improves resilience, scalability, and auditability
- Spring Boot + Kafka provides an ergonomic stack with
@KafkaListener,KafkaTemplate, and auto-configuration - Always handle errors gracefully: Use retries, dead letter topics, and manual acknowledgment for production systems
- Schema management with Avro and Schema Registry prevents breaking changes and ensures compatibility
- Consumers must be idempotent to handle at-least-once delivery semantics
- Test with
@EmbeddedKafkato validate producer/consumer behavior without external dependencies - Monitor consumer lag and metrics in production to detect issues early
Event-driven architecture isn’t just a buzzword—it’s a proven pattern for building systems that can grow with your business. With Kafka and Spring Boot, you have all the tools you need to implement it successfully. Start small, validate your event schemas early, and iterate.