Course topics

By WebNest Studio

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

Java
# 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();
    }
}
Output
Created exchange webnest.orders (topic), queues invoices.generate and invoices.generate.dlq, binding order.*.paid

Producer and competing consumers

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

Java
# 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);
}
Output
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

Java
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>() {});
Output
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.