DEV Community

Venkatesan Ramar
Venkatesan Ramar

Posted on

RabbitMQ vs Kafka: Choosing the Right Messaging System for Real Backend Architectures (part-3)

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
        );
    }
}

Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}

Enter fullscreen mode Exit fullscreen mode

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)