RabbitMQ with Spring Boot
RabbitMQ is a mature, general-purpose message broker built on the AMQP protocol. Its strength is a smart broker model: producers publish to an exchange, and routing rules called bindings decide which queues receive each message. Spring Boot’s spring-boot-starter-amqp wraps this with auto-configuration, a RabbitTemplate for sending, and the @RabbitListener annotation for consuming — so you declare topology as @Beans and let Spring wire the rest. This page builds a small order-notification flow end to end. See Messaging Intro for the broader picture.
Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
The starter pulls in Spring AMQP and the RabbitMQ Java client, and auto-configures a ConnectionFactory, a RabbitTemplate, and the @RabbitListener infrastructure.
Running RabbitMQ locally
The management image ships a web UI on port 15672 (default login guest/guest):
docker run -d --name rabbitmq \
-p 5672:5672 -p 15672:15672 \
rabbitmq:3.13-management
AMQP traffic uses port 5672; the management console lives at http://localhost:15672.
Connection configuration
Spring Boot connects to localhost:5672 with guest/guest out of the box. Override per environment in application.yml:
spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: auto # ack after the listener returns normally
retry:
enabled: true
max-attempts: 3
Tip: Keep credentials out of source. Use profiles and environment variables —
SPRING_RABBITMQ_PASSWORDoverrides the property automatically.
Declaring the topology as beans
The core of AMQP is exchange → binding → queue. A producer never publishes straight to a queue; it publishes to an exchange with a routing key, and bindings match that key to queues. We use a topic exchange, which routes on wildcard patterns (* = one word, # = zero or more words).
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitConfig {
public static final String EXCHANGE = "orders.exchange";
public static final String QUEUE = "orders.created.queue";
public static final String ROUTING_KEY = "orders.created";
@Bean
public TopicExchange ordersExchange() {
return new TopicExchange(EXCHANGE);
}
@Bean
public Queue ordersCreatedQueue() {
return QueueBuilder.durable(QUEUE).build(); // survives broker restarts
}
@Bean
public Binding ordersBinding(Queue ordersCreatedQueue, TopicExchange ordersExchange) {
return BindingBuilder.bind(ordersCreatedQueue)
.to(ordersExchange)
.with("orders.*"); // matches orders.created, orders.cancelled, ...
}
}
Spring AMQP declares these on the broker automatically at startup — no manual setup in the management UI.
| Exchange type | Routes by | Typical use |
|---|---|---|
direct | exact routing-key match | targeted point-to-point |
topic | wildcard pattern (*, #) | flexible event routing |
fanout | ignores key, broadcasts to all bound queues | pub/sub to everyone |
headers | message header attributes | routing on metadata |
JSON message conversion
By default Spring AMQP serializes payloads with Java serialization, which is brittle across services. Register a Jackson2JsonMessageConverter so messages travel as JSON — readable, language-neutral, and version-tolerant.
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory cf, MessageConverter converter) {
RabbitTemplate template = new RabbitTemplate(cf);
template.setMessageConverter(converter);
return template;
}
The same MessageConverter bean is picked up by @RabbitListener, so both sides agree on JSON. We send a record as the payload:
public record OrderCreated(Long orderId, String customer, double total) {}
Sending with RabbitTemplate
RabbitTemplate.convertAndSend(exchange, routingKey, payload) serializes the object via the converter and publishes it.
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OrderPublisher {
private final RabbitTemplate rabbitTemplate;
public void publish(OrderCreated order) {
rabbitTemplate.convertAndSend(
RabbitConfig.EXCHANGE,
RabbitConfig.ROUTING_KEY,
order);
}
}
The message on the wire:
Output (message body):
{ "orderId": 1042, "customer": "Asha", "total": 89.95 }
Consuming with @RabbitListener
Annotate a method with @RabbitListener(queues = ...) and Spring delivers each message, deserialized into the method’s parameter type using the same JSON converter.
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Component
public class OrderConsumer {
private static final Logger log = LoggerFactory.getLogger(OrderConsumer.class);
@RabbitListener(queues = RabbitConfig.QUEUE)
public void onOrderCreated(OrderCreated order) {
log.info("Received order {} for {} (${})",
order.orderId(), order.customer(), order.total());
// ... send confirmation email, reserve stock, etc.
}
}
Output (console):
2026-06-13T11:04:22.512 INFO OrderConsumer : Received order 1042 for Asha ($89.95)
With the default auto acknowledge mode, the message is acknowledged once the method returns normally. If the method throws, the message is redelivered (subject to the retry config above) and, when retries are exhausted, can be routed to a dead-letter queue.
Note: Consumers should be idempotent. At-least-once delivery means the same message can arrive twice after a redelivery, so processing it again must be safe — for example, by checking whether the order was already handled.
Dead-letter queues
Messages that repeatedly fail should not loop forever. Route them to a dead-letter exchange (DLX) for inspection by declaring the queue with DLX arguments:
@Bean
public Queue ordersCreatedQueue() {
return QueueBuilder.durable(RabbitConfig.QUEUE)
.withArgument("x-dead-letter-exchange", "orders.dlx")
.withArgument("x-dead-letter-routing-key", "orders.failed")
.build();
}
After retries are exhausted, the broker republishes the failed message to orders.dlx, where a separate queue and listener can log, alert, or store it.
Warning: Use durable queues and persistent messages (the default for
convertAndSend) for anything you cannot afford to lose. A non-durable queue is discarded when the broker restarts, taking its messages with it.
Best Practices
- Prefer a topic exchange for flexible event routing; reserve
fanoutfor true broadcast. - Always register
Jackson2JsonMessageConverterso payloads are JSON, not Java-serialized. - Make
@RabbitListenermethods idempotent and configure a dead-letter queue for poison messages. - Declare queues and exchanges as
durable; let Spring declare topology via@Beans. - Enable listener retry with a sane
max-attemptsinstead of retrying forever.