Spring Boot Tutorial
RabbitMQ with Spring Boot
RabbitMQ is a mature, widely deployed message broker built around queues and flexible routing. Where Kafka is a durable log for event streams, RabbitMQ shines at task distribution and routing: send a job to a queue and exactly one worker processes it; route messages by type or pattern to different queues; delay, prioritise, and dead-letter messages.
Spring AMQP, auto-configured by spring-boot-starter-amqp, gives you RabbitTemplate for sending and @RabbitListener for consuming. This lesson explains exchanges, queues and bindings, declares them in code, sends and receives JSON messages with Jackson 3, handles failures with retries and dead-letter queues, and implements request/reply.
Exchanges, Queues and Bindings
Producers never send directly to a queue; they publish to an exchange with a routing key. Bindings connect exchanges to queues with rules. Exchange types decide how routing works:
- Direct — deliver to queues whose binding key equals the routing key exactly.
- Topic — pattern matching on dot-separated keys:
order.*.created,order.#. - Fanout — broadcast to every bound queue, ignoring the key.
- Headers — route on message headers instead of the key.
Declaring the Topology
Declare Queue, Exchange and Binding beans (or use QueueBuilder/ExchangeBuilder/BindingBuilder). Spring's RabbitAdmin creates them on the broker at startup if they do not exist. Make queues durable so they survive broker restarts, and send persistent messages (the default in Spring AMQP).
Sending and Receiving JSON
Register a JacksonJsonMessageConverter bean (the Jackson 3 converter in Spring AMQP 4; Jackson2JsonMessageConverter is deprecated) and Spring Boot applies it to both RabbitTemplate and listener containers. convertAndSend(exchange, routingKey, object) sends; a @RabbitListener(queues = ...) method with a typed parameter receives. Several instances listening to the same queue act as competing consumers — each message goes to one of them.
Acknowledgements, Retries and Dead Letters
By default Spring acknowledges a message after the listener returns successfully and rejects it when the listener throws. Configure spring.rabbitmq.listener.simple.retry.* for in-process retries with back-off, and give the queue a dead-letter exchange so messages that still fail are moved to a DLQ instead of being redelivered forever. Throwing AmqpRejectAndDontRequeueException sends a message straight to the DLQ.
RabbitMQ or Kafka?
Choose RabbitMQ for work queues, complex routing, per-message TTL and priority, and request/reply. Choose Kafka for high-throughput event streams, replaying history, and many independent consumers of the same events. Many systems use both.
Examples
Setup and topology declaration
# compose.yaml
services:
rabbitmq:
image: rabbitmq:4-management
ports:
- "5672"
- "15672:15672" # management UI: http://localhost:15672 (guest/guest)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
@Configuration
public class RabbitConfig {
public static final String EXCHANGE = "webnest.orders";
public static final String INVOICE_QUEUE = "invoices.generate";
@Bean
TopicExchange ordersExchange() {
return ExchangeBuilder.topicExchange(EXCHANGE).durable(true).build();
}
@Bean
Queue invoiceQueue() {
return QueueBuilder.durable(INVOICE_QUEUE)
.deadLetterExchange("") // default exchange
.deadLetterRoutingKey(INVOICE_QUEUE + ".dlq")
.build();
}
@Bean
Queue invoiceDlq() {
return QueueBuilder.durable(INVOICE_QUEUE + ".dlq").build();
}
@Bean
Binding invoiceBinding(Queue invoiceQueue, TopicExchange ordersExchange) {
return BindingBuilder.bind(invoiceQueue).to(ordersExchange).with("order.*.paid");
}
@Bean
MessageConverter jsonConverter() {
return new JacksonJsonMessageConverter();
}
}
Created exchange webnest.orders (topic), queues invoices.generate and invoices.generate.dlq, binding order.*.paid
Producer and competing consumers
public record OrderPaid(String orderId, String customerEmail, BigDecimal amount) {}
@Service
public class PaymentEvents {
private final RabbitTemplate rabbit;
public PaymentEvents(RabbitTemplate rabbit) {
this.rabbit = rabbit;
}
public void orderPaid(OrderPaid event, String channel) {
// routing key like "order.web.paid" or "order.mobile.paid"
rabbit.convertAndSend(RabbitConfig.EXCHANGE, "order." + channel + ".paid", event);
}
}
@Component
public class InvoiceWorker {
private static final Logger log = LoggerFactory.getLogger(InvoiceWorker.class);
@RabbitListener(queues = RabbitConfig.INVOICE_QUEUE, concurrency = "2-5")
public void generate(OrderPaid event) {
log.info("Generating invoice for {} ({})", event.orderId(), event.amount());
// ... create PDF, store it, email it
}
}
orderPaid(WN-10231, "web") -> routed by "order.web.paid" to invoices.generate
[worker-1] Generating invoice for WN-10231 (2999.00)
orderPaid(WN-10232, "mobile") -> routed by "order.mobile.paid"
[worker-2] Generating invoice for WN-10232 (1499.00)
Listener retries, then dead-lettering
# application.yml
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
max-attempts: 4
initial-interval: 1s
multiplier: 2
default-requeue-rejected: false # after retries: dead-letter instead of requeue
@RabbitListener(queues = "invoices.generate.dlq")
public void inspect(OrderPaid failed, @Header(name = "x-death", required = false) List<Map<String, ?>> death) {
log.error("Invoice for {} failed permanently; death info {}", failed.orderId(), death);
}
WARN Retry 1/4 for WN-10240: PdfRenderingException
WARN Retry 2/4 ...
ERROR Invoice for WN-10240 failed permanently; death info [{reason=rejected, queue=invoices.generate, count=1}]
Request/reply: synchronous RPC over RabbitMQ
public record PriceQuote(String sku, BigDecimal price) {}
@Component
class PricingServer {
@RabbitListener(queues = "pricing.requests")
PriceQuote quote(String sku) { // return value is sent as the reply
return new PriceQuote(sku, pricing.currentPrice(sku));
}
}
// client
PriceQuote q = rabbit.convertSendAndReceiveAsType("", "pricing.requests", "HD-NAVY-M",
new ParameterizedTypeReference<PriceQuote>() {});
PriceQuote[sku=HD-NAVY-M, price=1299.00] (reply received via a temporary reply queue)
Common Mistakes
- Letting a failing message requeue forever, blocking the queue and burning CPU; configure retries and a dead-letter queue.
- Using the default Java serialization message converter instead of JSON, making messages unreadable to other languages.
- Declaring non-durable queues for important work, losing messages when the broker restarts.
- Assuming exactly-once delivery; consumers must be idempotent because redelivery can happen.
- Using RabbitMQ as a long-term event store; consumed messages are removed from the queue.
Key Points to Remember
- Producers publish to exchanges; bindings route messages to queues (direct, topic, fanout, headers).
- spring-boot-starter-amqp auto-configures RabbitTemplate and @RabbitListener containers.
- Use JacksonJsonMessageConverter (Jackson 3) for JSON messages in Spring AMQP 4.
- Configure listener retries plus dead-letter exchanges for failed messages.
- Pick RabbitMQ for task queues and routing, Kafka for replayable event streams.
Practice the examples
Change an input, predict the result, then compare it with the output. Explain why the result changes.
Use your local JDK or project IDE for these examples. Codelab currently runs Python and HTML/CSS/JavaScript; framework examples may need project dependencies.