This is my final part-3 of the series. I recommend you to read previous articles of the series.
In this article, I'd like to give sample code snippets for RabbitMQ & Kafka with Spring Boot.
9. Spring Boot Integration Examples
Messaging systems make a lot more sense once you see how they actually behave inside applications.
This section is not about building a full production-ready setup.
The goal here is simpler:
show how RabbitMQ and Kafka integrations usually feel different inside Spring Boot apps.
RabbitMQ Integration Example
RabbitMQ integration in Spring Boot is usually pretty simple and workflow-focused.
A typical flow looks something like this:
- order gets created,
- app publishes a processing task,
- consumer picks it up and runs business logic.
Producer Example
@Service
public class OrderPublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
public void publish(OrderCreatedEvent event) {
rabbitTemplate.convertAndSend(
"order.exchange",
"order.created",
event
);
}
}
Here:
- the exchange handles routing,
- routing keys decide where messages go, and
- RabbitMQ distributes messages to queues.
This routing flexibility is one of RabbitMQ’s biggest strengths.
Consumer Example
@Component
public class OrderConsumer {
@RabbitListener(queues = "order.processing.queue")
public void process(OrderCreatedEvent event) {
System.out.println("Processing order: " + event.orderId());
// Business logic
}
}
This style works really well for things like:
background jobs,
workflow execution,
notifications, and
transactional async tasks.
The queue basically acts like a work dispatcher.
Retry & DLQ Configuration
One reason RabbitMQ is popular in backend systems is its retry handling.
A common production setup usually includes:
- main queue,
- retry queue,
- dead-letter queue (DLQ).
@Bean
public Queue orderQueue() {
return QueueBuilder.durable("order.processing.queue")
.deadLetterExchange("order.dlx")
.build();
}
In real systems:
- temporary failures go through retry flows,
- poison messages move into DLQs, and
- teams get visibility into failed processing.
You’ll see this pattern everywhere in enterprise systems.
Kafka Integration Example
Kafka integration feels different because Kafka itself works differently.
Instead of queue-based task distribution, Kafka is built around event streams and partitioned logs.
Producer Example
@Service
public class OrderEventPublisher {
@Autowired
private KafkaTemplate<String, OrderCreatedEvent> kafkaTemplate;
public void publish(OrderCreatedEvent event) {
kafkaTemplate.send(
"order-events",
event.orderId(),
event
);
}
}
Notice this part:
event.orderId()
That’s the partition key.
And it matters a lot.
Kafka guarantees ordering only inside a partition.
Using the order ID as the partition key ensures:
- all events for the same order,
- stay inside the same partition, and
- remain ordered.
Partition strategy becomes a huge design topic in Kafka systems.
Consumer Example
@Component
public class OrderEventConsumer {
@KafkaListener(
topics = "order-events",
groupId = "order-processing-group"
)
public void consume(OrderCreatedEvent event) {
System.out.println("Processing order event: "
+ event.orderId());
// Business logic
}
}
Unlike RabbitMQ:
- Kafka consumers track offsets,
- messages stay in the log, and
- multiple consumer groups can process the same events independently.
That means:
- analytics services,
- audit systems,
- notification services,
- reporting pipelines
can all consume the same event stream separately.
This is one reason Kafka works so well for event-driven architectures.
Kafka Retry Handling
Retries in Kafka are usually handled using:
- retry topics,
- delayed retry topics, or
- custom consumer retry logic.
A common pattern looks like this:
- failed events move into retry topics,
- consumers retry later,
- poison messages eventually move into DLQs or parking-lot topics.
This setup is powerful, but definitely more operationally complex than RabbitMQ retry routing.
Kafka gives you more flexibility.
But it also expects more architectural discipline from the team.
The Bigger Architectural Difference
Even from the code examples, the difference becomes pretty obvious.
RabbitMQ apps usually feel:
- workflow-oriented,
- routing-focused, and
- delivery-centric.
Kafka apps usually feel:
- stream-oriented,
- event-centric, and
- partition-aware.
Neither one is universally better.
They’re just optimized for different kinds of problems.
And that difference becomes much more important once systems start scaling and production complexity kicks in.
10. Common Mistakes Teams Make
Most production messaging issues are not really caused by RabbitMQ or Kafka.
They usually happen because of:
- bad assumptions,
- over-engineering, or
- missing operational visibility.
And honestly, the same mistakes show up again and again across teams.
Using Kafka as a Task Queue
This one happens a lot.
Kafka is amazing for:
- event streaming,
- analytics,
- replayability, and
- handling huge event volumes.
But teams sometimes use it for very simple things like:
- background jobs,
- workflow execution, or
- async task processing.
That usually brings in:
- partition management,
- retry complexity,
- consumer coordination, and
- extra operational overhead.
If the actual requirement is just:
“Run tasks reliably in the background”
RabbitMQ is often the cleaner and simpler solution.
Not every async workflow needs a distributed event streaming platform.
Sometimes a queue is just a queue.
Choosing Kafka Just Because It “Scales Better”
Yes, Kafka scales extremely well.
But scalability only matters when you actually need it.
A lot of systems never reach the scale where Kafka’s architecture becomes necessary.
Meanwhile, the team still has to deal with:
- partitions,
- retention policies,
- lag monitoring,
- broker management, and
- cluster operations.
That’s a lot of complexity to carry around for no real reason.
Good architecture solves real problems — not imaginary future scale problems.
Ignoring Idempotency
Retries eventually create duplicates.
Always assume that.
This applies to both RabbitMQ and Kafka.
If consumers are not idempotent:
- payments may run twice,
- emails may send twice,
- inventory may break,
- workflows may repeat unexpectedly.
Messaging guarantees alone won’t save you here.
Applications still need:
- deduplication logic,
- safe retry handling, and
- idempotent consumers.
Experienced engineers usually assume duplicate delivery will happen eventually.
Because in distributed systems, it eventually does.
Treating RabbitMQ Like Event Storage
RabbitMQ is built for message delivery.
Not long-term event retention.
Trying to build:
- replayable event history,
- event sourcing systems, or
- analytics pipelines
on top of RabbitMQ usually becomes painful later.
Kafka is naturally better for those workloads.
Using the wrong abstraction eventually creates operational headaches.
Over-Partitioning Kafka
Partitions help with parallelism.
But too many partitions create their own problems:
- rebalance overhead,
- broker pressure,
- operational complexity, and
- consumer coordination costs.
More partitions do not automatically mean better performance.
Partition strategy should match:
- throughput requirements,
- scaling needs, and
- ordering guarantees.
Bad partition planning becomes very hard to fix later.
Ignoring Observability
Teams generally monitor broker uptime and stop there.
But healthy messaging systems need much deeper visibility.
You usually want to monitor:
- queue depth,
- consumer lag,
- retry rates,
- DLQ growth,
- processing latency, and
- throughput trends.
Distributed systems rarely fail instantly.
Problems usually build slowly over time.
Without observability, teams often discover issues only after customers complain.
11. Decision Matrix
At this point, the pattern becomes pretty obvious:
RabbitMQ and Kafka solve different kinds of problems.
They are not direct replacements for each other in every scenario.
Here’s a simple decision guide.
| Scenario | Better Fit | Why |
|---|---|---|
| Background job processing | RabbitMQ | Simpler retries and task distribution |
| Workflow orchestration | RabbitMQ | Flexible routing and operational simplicity |
| Notification systems | RabbitMQ | Easy fanout and retry handling |
| Payment workflows | RabbitMQ | Better delivery-focused control |
| Event streaming | Kafka | High-throughput distributed event log |
| Real-time analytics | Kafka | Replayability and scalable consumers |
| Audit systems | Kafka | Durable event retention |
| Event sourcing | Kafka | Immutable event history |
| CDC pipelines | Kafka | Stream-first architecture |
| Simple async microservice communication | RabbitMQ | Lower operational overhead |
| Large-scale event platforms | Kafka | Built for distributed streaming |
A Practical Rule of Thumb
A simple rule usually works well:
Choose RabbitMQ when the main concern is:
- task execution,
- workflow coordination,
- retries, and
- operational simplicity.
Choose Kafka when the main concern is:
- event streaming,
- replayability,
- analytics, and
- long-term event retention.
That distinction alone clears up a lot of confusion early in system design.
Final Thoughts
RabbitMQ and Kafka are both excellent technologies and were designed with very different goals.
Good engineering is not about picking the most impressive or cutting-edge technology.
It’s about choosing the technology that fits naturally, stays maintainable, and behaves predictably under real production pressure.
Many mature systems eventually use both RabbitMQ and Kafka together.
The important part is knowing where each one actually fits best.
Appreciate your support and suggestions.
Top comments (0)