RabbitMQ Tutorial: Learn Message Queue from Scratch (2026)
RabbitMQ is the most widely deployed open-source message broker. It implements AMQP 0-9-1 and supports multiple messaging patterns — direct, topic, fanout, headers exchanges, RPC, publisher confirms, and dead-letter queues. I have used RabbitMQ for task distribution, event notifications, and RPC patterns across microservices in production environments requiring reliable async communication.
This tutorial covers exchanges, queues, bindings, Spring AMQP integration, publisher confirms, dead-letter queues, and cluster setup.
Exchanges, Queues, and Bindings
Producers send messages to exchanges, not queues directly. Exchange types: direct (routing key match), topic (pattern match with wildcards), fanout (broadcast to all bound queues), headers (match header attributes). Bindings connect queues to exchanges with routing rules that determine which messages enter which queues.
Choose the exchange type based on your routing needs. Topic exchanges cover most use cases with pattern-based routing (order.created, order.updated). Fanout exchanges are useful for broadcasting events to multiple consumers. Direct exchanges work for point-to-point communication.
# Declare using RabbitMQ CLI
rabbitmqadmin declare exchange name=orders type=topic durable=true
rabbitmqadmin declare queue name=order.payment durable=true
rabbitmqadmin declare binding source=orders destination=order.payment routing_key="payment.*"
# Spring Boot configuration
@Configuration
public class RabbitConfig {
@Bean
public TopicExchange ordersExchange() {
return new TopicExchange("orders", true, false);
}
@Bean
public Queue paymentQueue() {
return QueueBuilder.durable("order.payment").build();
}
@Bean
public Binding paymentBinding() {
return BindingBuilder.bind(paymentQueue())
.to(ordersExchange()).with("payment.*");
}
}
Producing and Consuming with Spring AMQP
RabbitTemplate sends messages with exchange and routing key. @RabbitListener on methods consumes from queues automatically. Configure Jackson2JsonMessageConverter for POJO serialization. Use MessageProperties for headers and delivery metadata like priority or expiration.
// Sending
@Autowired
private RabbitTemplate rabbitTemplate;
public void sendPaymentEvent(PaymentEvent event) {
rabbitTemplate.convertAndSend("orders", "payment.created", event);
}
// Receiving
@Component
public class PaymentConsumer {
@RabbitListener(queues = "order.payment")
public void handlePayment(PaymentEvent event) {
log.info("Received payment: {}", event.getOrderId());
processPayment(event);
}
}
// Configuration
@Bean
public Jackson2JsonMessageConverter jsonConverter() {
return new Jackson2JsonMessageConverter();
}
Publisher Confirms and Returns
Publisher confirms guarantee that the broker has received a message. Enable with publisher-confirm-type=correlated. Returns notify when a message cannot be routed to any queue. This combination provides reliable message delivery with no data loss, essential for financial transactions and critical workflows.
spring.rabbitmq.publisher-confirm-type=correlated
spring.rabbitmq.publisher-returns=true
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate template = new RabbitTemplate(connectionFactory);
template.setMandatory(true);
template.setConfirmCallback((correlationData, ack, cause) -> {
if (!ack) log.error("Message not confirmed: {}", cause);
});
template.setReturnsCallback(returned -> {
log.error("Message returned: {} -> {}.{}",
returned.getMessage(), returned.getExchange(), returned.getRoutingKey());
});
return template;
}
Dead-Letter Queues
Messages that cannot be processed (rejected, TTL expired, queue full) route to a dead-letter exchange (DLX). Configure DLX on the main queue. DLX-bound queues capture failures for later analysis, retry, or alerting. This pattern is essential for production reliability — without DLQs, failed messages are silently lost.
@Bean
public Queue paymentQueue() {
return QueueBuilder.durable("order.payment")
.deadLetterExchange("orders.dlx")
.deadLetterRoutingKey("payment.failed")
.ttl(30000)
.maxLength(10000)
.build();
}
@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable("order.payment.dlq").build();
}
@Bean
public Binding dlqBinding() {
return BindingBuilder.bind(deadLetterQueue())
.to(new DirectExchange("orders.dlx")).with("payment.failed");
}
Cluster and High Availability
RabbitMQ clusters aggregate nodes for throughput and availability. Quorum queues provide data replication and consistency across nodes. For cross-datacenter scenarios, use the Federation plugin or Shovel to move messages between clusters. Monitor queue depth and consumer lag as key health indicators.
# Node 1
RABBITMQ_NODENAME=rabbit@node1 rabbitmq-server -detached
rabbitmqctl cluster_status
# Join node2 to cluster
rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@node1
rabbitmqctl start_app
# Declare a quorum queue
rabbitmqadmin declare queue name=order.payment durable=true arguments='{"x-queue-type":"quorum"}'
# Enable federation
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
Retry and Error Handling Strategies
Message processing failures require careful handling. Configure retry via Spring Retry with exponential backoff — failed messages are requeued with a delay. After exhausting retries, messages route to the dead-letter queue. Use a separate retry interceptor that wraps the @RabbitListener with configurable max attempts and backoff policy.
For idempotent consumers, track processed message IDs in a database or cache to detect and skip duplicates. This is essential when using at-least-once delivery guarantees, as messages may be redelivered after consumer failures or connection drops.
@Bean
public SimpleRabbitListenerContainerFactory retryContainerFactory(
ConnectionFactory connectionFactory) {
SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setAdviceChain(RetryInterceptorBuilder.stateless()
.maxAttempts(3)
.backoffOptions(1000, 2.0, 10000) // 1s, 2s, 4s
.recoverer(new RejectAndDontRequeueRecoverer())
.build());
return factory;
}
@Component
public class OrderConsumer {
@RabbitListener(queues = "order.processing",
containerFactory = "retryContainerFactory")
public void processOrder(OrderEvent event) {
// Idempotency check
if (processedOrderIds.exists(event.getOrderId())) {
log.info("Order {} already processed", event.getOrderId());
return;
}
processPayment(event);
processedOrderIds.record(event.getOrderId());
}
}
Frequently Asked Questions
What is the difference between RabbitMQ and Kafka?
RabbitMQ is a message broker with flexible routing and delivery guarantees. Kafka is an event streaming platform with disk persistence and replay. RabbitMQ excels at task distribution; Kafka excels at event streaming at scale.
What exchange type should I use?
Direct for point-to-point, topic for pattern-based routing (e.g., order.created, order.updated), fanout for broadcasts, headers for complex attribute-based routing. Topic exchanges cover most enterprise use cases.
How do I handle message reprocessing?
Use dead-letter queues for failed messages. A separate consumer reads from the DLQ, logs the failure, optionally retries after a delay, or routes to a manual intervention queue for human review.
What is the difference between queues and exchanges?
Exchanges receive messages from producers and route them to queues based on binding rules. Queues store messages until consumers consume them. Producers never write directly to queues — always to exchanges.
Originally published on Ayodhyyya. Last updated June 1, 2026.