RabbitMQ with Spring Boot (AMQP)
RabbitMQ is a message broker that routes messages to queues using exchanges. It is a good fit for task queues, work distribution and flexible routing, where each message is handled by one consumer and then removed.
AMQP model
Producer ─► Exchange ── binding (routing key rule) ──► Queue ──► Consumer
│ └─────► Consumer (competing consumers share the queue)
├── binding ──► Queue B ──► Consumer
The producer never sends to a queue directly. It sends to an EXCHANGE with a ROUTING KEY.
The exchange type decides which queues get the message.| Exchange type | Routing | Example |
|---|---|---|
| direct | Exact routing key match | order.created goes to the order queue |
| topic | Pattern match (* one word, # many words) | order.* or payment.# |
| fanout | All bound queues, key ignored | Broadcast to every service |
| headers | Match on message headers | Rare |
Dependency and configuration
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>spring:
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
listener:
simple:
acknowledge-mode: auto
prefetch: 10
concurrency: 2
max-concurrency: 8
retry:
enabled: true
initial-interval: 1s
max-attempts: 4
multiplier: 2.0Declare exchange, queues, dead letter queue
@Configuration
public class RabbitConfig {
public static final String EXCHANGE = "shop.exchange";
public static final String QUEUE = "order.created.queue";
public static final String DLX = "shop.dlx";
public static final String DLQ = "order.created.dlq";
@Bean TopicExchange shopExchange() { return new TopicExchange(EXCHANGE); }
@Bean DirectExchange deadLetterExchange() { return new DirectExchange(DLX); }
@Bean
Queue orderQueue() {
return QueueBuilder.durable(QUEUE)
.withArgument("x-dead-letter-exchange", DLX) // rejected messages go here
.withArgument("x-dead-letter-routing-key", DLQ)
.build();
}
@Bean Queue deadLetterQueue() { return QueueBuilder.durable(DLQ).build(); }
@Bean Binding orderBinding() { return BindingBuilder.bind(orderQueue()).to(shopExchange()).with("order.created"); }
@Bean Binding dlqBinding() { return BindingBuilder.bind(deadLetterQueue()).to(deadLetterExchange()).with(DLQ); }
@Bean
MessageConverter jsonConverter() { return new Jackson2JsonMessageConverter(); } // JSON instead of Java serialization
}Producer and consumer
public record OrderCreated(String orderId, BigDecimal total) { }
@Service
public class OrderPublisher {
private final RabbitTemplate rabbit;
public OrderPublisher(RabbitTemplate rabbit) { this.rabbit = rabbit; }
public void publish(OrderCreated event) {
rabbit.convertAndSend(RabbitConfig.EXCHANGE, "order.created", event);
}
}
@Component
public class OrderConsumer {
@RabbitListener(queues = RabbitConfig.QUEUE)
public void handle(OrderCreated event) {
if (event.total().signum() < 0) {
throw new AmqpRejectAndDontRequeueException("invalid total"); // straight to the dead letter queue
}
System.out.println("Processing " + event.orderId()); // exception = retry, then DLQ
}
}Message life cycle and acknowledgements
1. Producer publishes (optionally with publisher confirms: broker confirms it stored the message)
2. Exchange routes to queue(s) (if no queue matches the message is dropped, unless mandatory/returns are set)
3. Broker delivers up to `prefetch` unacknowledged messages to a consumer
4. Listener succeeds ─► ack ─► message removed from the queue
Listener throws ─► retry (spring retry) ─► still failing ─► reject without requeue ─► dead letter exchange ─► DLQ
5. Consumer dies before ack ─► broker requeues the message to another consumer- Make queues and messages durable so they survive a broker restart. Spring's default persistent delivery mode does this for messages.
- Set prefetch small (for example 10) so slow consumers do not hoard messages and fast ones stay busy.
- Enable publisher confirms (spring.rabbitmq.publisher-confirm-type=correlated) for important messages.
- Requeue loops: never requeue a message that can never succeed, or it will spin forever. Use the DLQ.
RabbitMQ or Kafka?
| Point | RabbitMQ | Kafka |
|---|---|---|
| Model | Smart broker, message removed after ack | Log, consumers keep offsets, data kept by retention |
| Routing | Very flexible (exchanges) | Topic and partition only |
| Replay | No (unless streams plugin) | Yes |
| Ordering | Per queue with one consumer | Per partition |
| Best for | Task queues, RPC style, complex routing | Event streams, analytics, event sourcing, very high volume |
Common mistake: Using the default guest user and no dead letter queue in production. Create real users, and always define a dead letter queue so failing messages are stored for inspection and not lost or endlessly retried.
Interview questions
What is the difference between a queue and an exchange?
A queue stores messages. An exchange receives messages from producers and routes them to queues using bindings and routing keys.
How do I process messages in parallel?
Use several consumers on the same queue (competing consumers) with concurrency and max-concurrency. Ordering between them is not guaranteed.
What happens if the broker restarts?
Durable queues and persistent messages survive. Non-durable queues and transient messages are lost.