Spring Boot: RabbitMQ

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Every other RabbitMQ page in this section — RabbitMQ Architecture and the AMQP Model, Publishing to RabbitMQ, Consuming from RabbitMQ, RabbitMQ Queue Types, RabbitMQ Routing Patterns, RabbitMQ Reliability, Dead-Lettering and TTL and RabbitMQ and AMQP 1.0 — works directly against the com.rabbitmq.client Java client. This page is the Spring layer on top of all of them: how the same exchanges, queues, bindings, publisher confirms, acknowledgement modes and AMQP 1.0 concepts those pages already explained show up as Spring beans, annotations and spring.rabbitmq.* properties once spring-boot-starter-amqp is on the classpath. It assumes that protocol-level vocabulary throughout and cross-references it instead of repeating it.

The baseline is Spring Boot 4.1.x on Spring Framework 7.1.x, Spring AMQP 4.1.x (spring-rabbit), against a RabbitMQ 4.3.x broker, running on a Java 25 runtime with Java 21 as the current LTS floor. Spring AMQP 4.0 deprecated the Jackson 2-based converter family for removal and 4.1 replaced it outright with Jackson 3-based types, which matters concretely below; every class, method and property name here has been checked against the current docs.spring.io reference rather than carried over from older Spring Boot 1.x/2.x material, since a good deal of published RabbitMQ-with-Spring-Boot content (including the RabbitMQ chapter of a well-known 2017 book written against Spring Boot 1.4 and RabbitMQ 3.6) predates the current container types, converters, property names and even the removal of classic mirrored queues covered on RabbitMQ Queue Types.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

spring-boot-starter-amqp brings in spring-rabbit and the RabbitMQ Java client, and triggers Spring Boot’s RabbitAutoConfiguration, which wires a CachingConnectionFactory, a RabbitTemplate, a RabbitAdmin and a default SimpleRabbitListenerContainerFactory from spring.rabbitmq.* properties, all without any @Bean method of your own.

Dependency and connection configuration

Property Purpose

spring.rabbitmq.addresses

A comma-separated list of host:port pairs (or full amqp://user:pass@host:port URIs) for a cluster; when set, host/port are ignored, and an amqps:// scheme turns SSL on automatically.

spring.rabbitmq.host / .port / .username / .password

Single-node connection details, used only when addresses is not set.

spring.rabbitmq.virtual-host

The vhost to connect to.

spring.rabbitmq.ssl.enabled / .bundle

Turns TLS on; ssl.bundle names a Spring Boot SslBundle (keystore/truststore, or PEM material) instead of the older discrete key-store/trust-store properties, which still exist for a bundle-free setup.

spring.rabbitmq.connection-timeout / .requested-heartbeat

Passed through to the underlying RabbitMQ Java ConnectionFactory — the same heartbeat negotiation covered on RabbitMQ Reliability, Dead-Lettering and TTL.

spring:
  rabbitmq:
    addresses: node1.rabbit.internal:5671,node2.rabbit.internal:5671,node3.rabbit.internal:5671
    virtual-host: orders
    username: orders-service
    password: ${RABBITMQ_PASSWORD}
    ssl:
      enabled: true
      bundle: rabbitmq

See Spring Boot Reference — AMQP for the complete auto-configuration this feeds, including RabbitConnectionDetails (used to source these values from a ServiceConnection instead of plain properties in a container/Testcontainers setup) and the SSL bundle mechanism.

Sending messages: RabbitTemplate, confirms and returns

AmqpTemplate/RabbitTemplate mirror channel.basicPublish from Publishing to RabbitMQ one level up: send(exchange, routingKey, message) takes a raw org.springframework.amqp.core.Message, and convertAndSend(exchange, routingKey, object) runs the payload through the configured MessageConverter (below) first. Both default to whatever template.exchange/template.routing-key are set to when the exchange/routing key arguments are omitted.

@Service
public class OrderPublisher {

    private final RabbitTemplate rabbitTemplate;

    public OrderPublisher(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public void publish(Order order) {
        rabbitTemplate.convertAndSend("orders", "orders.eu.created", order);
    }
}

Publisher confirms and returns — the same two mechanisms Publishing to RabbitMQ covers at the AMQP level (confirmSelect, the mandatory flag) — are turned on declaratively:

spring:
  rabbitmq:
    publisher-confirm-type: correlated   # wraps the underlying channel in publisher-confirms mode
    publisher-returns: true              # enables basic.return callbacks for unroutable messages
    template:
      mandatory: true                    # this template's sends set the mandatory flag
      retry:
        enabled: true                    # retries a send if the broker connection is temporarily unavailable
        initial-interval: 2s
        max-retries: 3

With those set, a RabbitTemplate registers exactly one ConfirmCallback and one ReturnsCallback each, invoked on one of the connection factory’s own executor threads rather than the AMQP client’s I/O thread:

@Configuration
public class RabbitTemplateConfig {

    @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
            MessageConverter messageConverter) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        template.setMessageConverter(messageConverter);
        template.setMandatory(true);

        // confirm: broker accepted (ack=true) or could not handle (ack=false, cause explains why) the publish
        template.setConfirmCallback((correlationData, ack, cause) -> {
            if (!ack) {
                log.warn("publish not confirmed, correlationId={}, cause={}",
                        correlationData != null ? correlationData.getId() : null, cause);
            }
        });

        // return: mandatory=true and the exchange could not route the message to any queue
        template.setReturnsCallback(returned ->
                log.warn("message returned, exchange={}, routingKey={}, replyText={}",
                        returned.getExchange(), returned.getRoutingKey(), returned.getReplyText()));

        return template;
    }
}

Correlating a specific send with its confirm uses CorrelationData, passed as an extra argument to send/ convertAndSend; its own getFuture() returns a CompletableFuture<Confirm> as an alternative to the shared ConfirmCallback, and when both confirms and returns are enabled, a returned message ends up on correlationData.getReturn() too, guaranteed to be set before the confirm future completes:

CorrelationData correlationData = new CorrelationData(order.id());
rabbitTemplate.convertAndSend("orders", "orders.eu.created", order, correlationData);

correlationData.getFuture().whenComplete((confirm, ex) -> {
    if (ex != null || !confirm.isAck()) {
        log.warn("order {} not confirmed: {}", order.id(), confirm != null ? confirm.getReason() : ex);
    }
});

See Spring AMQP Reference — Sending Messages and Spring AMQP Reference — Using RabbitTemplate (its "Correlated Publisher Confirms and Returns" section) for the full ConfirmCallback/ ReturnsCallback/CorrelationData contract this builds on.

Declaring topology as Spring beans

Exchanges, queues and bindings declared as @Bean`s are picked up automatically by the auto-configured `RabbitAdmin and (re-)declared to the broker on every connection, including after a connection recovery — no separate AmqpAdmin.declareQueue(…​) call needed for anything defined this way.

@Configuration
public class OrdersTopologyConfig {

    @Bean
    public TopicExchange ordersExchange() {
        return new TopicExchange("orders");
    }

    @Bean
    public Queue ordersQueue() {
        return QueueBuilder.durable("orders")
                .quorum()                          // x-queue-type=quorum -- see RabbitMQ Queue Types
                .deadLetterExchange("orders.dlx")
                .build();
    }

    @Bean
    public Binding ordersBinding() {
        return BindingBuilder.bind(ordersQueue()).to(ordersExchange()).with("orders.*");
    }
}

QueueBuilder and ExchangeBuilder are the fluent equivalent of the constructors RabbitMQ Architecture and the AMQP Model introduces at the AMQP level: QueueBuilder.durable(name)/.nonDurable(name) starts the chain, .quorum()/ .stream() set x-queue-type the same way RabbitMQ Queue Types does directly, and well-known arguments (ttl, maxLength, deadLetterExchange, maxPriority, singleActiveConsumer, and so on) are typed builder methods instead of a raw Map<String, Object>. DirectExchange, TopicExchange, FanoutExchange and HeadersExchange map directly onto the four exchange types from that same page, and BindingBuilder.bind(queue).to(exchange).with(routingKey) (or .matching(…​) for a headers exchange) replaces channel.queueBind.

Grouping several Declarable beans — queues, exchanges and bindings together — under one Declarables bean avoids one @Bean method per object where that gets repetitive, and Declarables#getDeclarablesByType retrieves a typed subset back out of it (useful when wiring a listener container’s setQueues(…​) from the same beans the admin already declared):

@Bean
public Declarables ordersTopology() {
    return new Declarables(
            new TopicExchange("orders"),
            QueueBuilder.durable("orders").quorum().build(),
            QueueBuilder.durable("orders.dlq").build(),
            BindingBuilder.bind(new Queue("orders")).to(new TopicExchange("orders")).with("orders.*"));
}

Injecting AmqpAdmin directly is for anything genuinely dynamic — a queue whose name or arguments are only known at runtime, not declarable as a fixed bean:

@Component
public class TenantQueueProvisioner {

    private final AmqpAdmin amqpAdmin;

    public TenantQueueProvisioner(AmqpAdmin amqpAdmin) {
        this.amqpAdmin = amqpAdmin;
    }

    public void provisionQueueFor(String tenantId) {
        amqpAdmin.declareQueue(QueueBuilder.durable("orders." + tenantId).quorum().build());
    }
}

See Spring AMQP Reference — Broker Configuration for the complete QueueBuilder/ExchangeBuilder/BindingBuilder/Declarables/AmqpAdmin API, including conditional declaration across multiple RabbitAdmin instances (relevant to Multiple brokers below).

Consuming messages: @RabbitListener

@RabbitListener is the annotated equivalent of channel.basicConsume from Consuming from RabbitMQ. Its simplest form just names an already-declared queue:

@Component
public class OrderListener {

    @RabbitListener(queues = "orders")
    public void onOrder(Order order) {
        process(order);
    }
}

bindings = @QueueBinding(…​) folds the topology declaration from the previous section into the listener itself — convenient for a self-contained listener, at the cost of spreading topology across annotations instead of keeping it in one @Configuration class:

@Component
public class OrderListener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "orders", durable = "true"),
            exchange = @Exchange(value = "orders", type = ExchangeTypes.TOPIC),
            key = "orders.*"))
    public void onOrder(Order order) {
        process(order);
    }
}

containerFactory selects which RabbitListenerContainerFactory bean backs this listener (see the container comparison below); concurrency sets the invoker thread count, either a fixed number or a "min-max" range string such as "2-4" for a SimpleMessageListenerContainer to scale within; id names the underlying listener container bean for later lookup through RabbitListenerEndpointRegistry; errorHandler names a RabbitListenerErrorHandler bean invoked with the raw message, the converted Message<?> and the wrapping ListenerExecutionFailedException, which can itself return a reply or rethrow; and returnExceptions, when true, sends a thrown exception back to the sender (wrapped in a RemoteInvocationResult) instead of only surfacing it to the container.

@RabbitListener(queues = "orders", containerFactory = "quorumContainerFactory",
        concurrency = "2-4", errorHandler = "orderListenerErrorHandler")
public void onOrder(Order order) {
    process(order);
}

Acknowledgement mode itself is not an @RabbitListener attribute on the classic AMQP 0-9-1 path — it is a property of the container factory (setAcknowledgeMode(AcknowledgeMode.MANUAL), or spring.rabbitmq.listener.simple.acknowledge-mode/.direct.acknowledge-mode), consistently with AUTO/MANUAL/NONE mapping onto the automatic/manual/no-ack modes Consuming from RabbitMQ covers at the AMQP level. Under manual acknowledgement, a listener method’s own thrown exception is what tells the container to basic.nack/basic.reject; throwing AmqpRejectAndDontRequeueException specifically forces requeue=false regardless of the container’s defaultRequeueRejected setting, which is exactly how a listener opts a message out of redelivery and into a configured dead-letter exchange (see retry and dead lettering below).

Multiple methods on one listener: @RabbitHandler

A single @RabbitListener at the class level, combined with @RabbitHandler on several methods, lets one listener endpoint dispatch to a different method by payload type after conversion — useful when one queue legitimately carries more than one message shape:

@RabbitListener(queues = "orders.events")
@Component
public class OrderEventListener {

    @RabbitHandler
    public void onCreated(OrderCreated event) {
        process(event);
    }

    @RabbitHandler
    public void onCancelled(OrderCancelled event) {
        process(event);
    }

    @RabbitHandler(isDefault = true)
    public void onUnknown(Object event) {
        log.warn("unrecognized order event: {}", event);
    }
}

See Spring AMQP Reference — Annotation-Driven Listener Endpoints for the full @RabbitListener/@QueueBinding/@RabbitHandler attribute reference, and Spring AMQP Reference — Handling Exceptions for errorHandler/returnExceptions in full.

Choosing a listener container

Spring AMQP ships two listener container implementations, selected per factory bean (or globally through spring.rabbitmq.listener.type, simple by default or direct):

SimpleMessageListenerContainer (SMLC)

DirectMessageListenerContainer (DMLC)

Threading model

A dedicated internal thread per consumer, fed from the RabbitMQ client’s own connection thread through an internal queue — one extra hand-off per delivery.

The listener runs directly on the RabbitMQ client’s own consumer thread — no internal queue, no extra hand-off.

Concurrency knob

concurrentConsumers (fixed) or maxConcurrentConsumers with auto-scaling (consecutiveActiveTrigger, consecutiveIdleTrigger, and related tuning), or the combined concurrency range string.

consumersPerQueue, adjustable at runtime without restarting the container; no built-in auto-scaling.

Adding/removing queues at runtime

Restarts the whole set of consumer threads for the container.

Only the affected queue’s consumer is added or cancelled — the others are unaffected.

Batching

batchSize/consumerBatchEnabled supported, including inside a transaction.

No batchSize with transactions; messagesPerAck reduces ack frequency instead.

Container factory bean

SimpleRabbitListenerContainerFactory

DirectRabbitListenerContainerFactory

SMLC remains the default and the better fit when transactional batching or maxConcurrentConsumers auto-scaling matters; DMLC is the better fit for a service whose set of consumed queues changes at runtime (multi-tenant queue-per-tenant patterns, for example) or that wants to avoid the extra internal thread hand-off per delivery.

spring:
  rabbitmq:
    listener:
      type: direct
      direct:
        consumers-per-queue: 3
        acknowledge-mode: manual
      simple:
        concurrency: 2
        max-concurrency: 8
        acknowledge-mode: manual
@Bean
public DirectRabbitListenerContainerFactory quorumContainerFactory(ConnectionFactory connectionFactory,
        MessageConverter messageConverter) {
    DirectRabbitListenerContainerFactory factory = new DirectRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(messageConverter);
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
    factory.setConsumersPerQueue(3);
    return factory;
}

See Spring AMQP Reference — Choosing a Container for the complete decision guidance and Spring AMQP Reference — Listener Concurrency for the auto-scaling algorithm and the concurrency range-string syntax in full.

Message converters and type mapping

The default MessageConverter is SimpleMessageConverter (Java serialization for a Serializable payload, or a raw byte[]/String pass-through) — essentially never what a real service wants. Registering a JacksonJsonMessageConverter bean is picked up automatically by both the auto-configured RabbitTemplate and the default listener container factory:

@Bean
public MessageConverter messageConverter() {
    return new JacksonJsonMessageConverter();
}

Spring AMQP 4.0 deprecated the Jackson 2-based AbstractJackson2MessageConverter family (and Jackson2JavaTypeMapper) for removal in favor of Jackson 3-based replacements, and 4.1 ships only the Jackson 3-based types — JacksonJsonMessageConverter and DefaultJacksonJavaTypeMapper (DefaultClassMapper) — rather than the Jackson2JsonMessageConverter still found in older Spring Boot examples.

On send, the converter stamps the payload’s fully-qualified class name into the TypeId message header (plus ContentTypeId/KeyTypeId for a Map’s value/key types); on receive, it reads that same header back to pick the target Java type, which only works when publisher and consumer share the same class on their respective classpaths. `DefaultClassMapper#setIdClassMapping decouples the two sides by mapping an arbitrary string id to a local type instead, and trustedPackages is the deserialization allow-list a converter needs before it will instantiate anything outside java.util/java.lang by default — both matter as soon as publisher and consumer are different services rather than the same codebase:

@Bean
public MessageConverter messageConverter() {
    JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();

    DefaultClassMapper classMapper = new DefaultClassMapper();
    classMapper.setTrustedPackages("com.example.orders.events");
    classMapper.setIdClassMapping(Map.of(
            "order-created", OrderCreated.class,
            "order-cancelled", OrderCancelled.class));
    converter.setClassMapper(classMapper);

    return converter;
}

A method parameter’s own declared type is used by default in preference to the inbound TypeId header (TypePrecedence.INFERRED, the default); setting TypePrecedence.TYPE_ID on the converter instead trusts the sender’s header over the listener method’s own signature, relevant mainly to the @RabbitHandler multi-method dispatch above.

See Spring AMQP Reference — Message Conversion for the complete converter hierarchy (including JacksonXmlMessageConverter and content-type-aware conversion), and Spring Boot Reference — AMQP for how a MessageConverter bean is picked up by the auto-configured template and default container factory without further wiring.

Acknowledgement, retry and dead lettering

RabbitMQ Reliability, Dead-Lettering and TTL already covers dead-letter exchanges, the four dead-letter reasons and TTL/queue-length-driven dead-lettering in full at the protocol level — a queue declared with .deadLetterExchange(…​) on QueueBuilder above already gets all of that. What is specific to the Spring layer is how a listener, rather than the broker, decides a message should not be retried and should go straight to dead-lettering: throwing AmqpRejectAndDontRequeueException from a listener method does exactly that, unconditionally.

A RetryInterceptorBuilder-built interceptor sits in front of that broker-level dead-lettering, retrying a failed delivery a bounded number of times, in-process, before it ever counts as rejected:

@Bean
public RetryOperationsInterceptor listenerRetryInterceptor(RabbitTemplate rabbitTemplate) {
    return RetryInterceptorBuilder.stateless()
            .maxRetries(3)
            .backOffOptions(1_000, 2.0, 10_000) // initialInterval, multiplier, maxInterval
            .recoverer(new RepublishMessageRecoverer(rabbitTemplate, "orders.dlx", "orders.failed"))
            .build();
}

@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
        ConnectionFactory connectionFactory, MessageConverter messageConverter,
        RetryOperationsInterceptor listenerRetryInterceptor) {
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(messageConverter);
    factory.setAdviceChain(listenerRetryInterceptor);
    return factory;
}

Stateless retry (RetryInterceptorBuilder.stateless()) keeps the failing delivery on the same thread and retries it in a loop, appropriate when there is no surrounding transaction, or one is started inside the retry callback itself. Stateful retry (.stateful()) is for the opposite case — a transaction already started higher up the call stack, where the delivery has to be returned to the broker and redelivered for each attempt instead of looped locally — and needs a way to recognize the same delivery across those redeliveries, normally the message’s MessageId property (createMessageIds on the converter, or a custom MessageKeyGenerator). RepublishMessageRecoverer above is what actually runs once retries are exhausted — it republishes the message, with the failure’s exception message and original exchange/routing key added as headers, to wherever it’s told (here, straight to the same dead-letter exchange the queue’s own x-dead-letter-exchange would have used) and then acks the original delivery, which is what keeps the broker’s own dead-lettering from also firing on the same message. RejectAndDontRequeueRecoverer, the default recoverer if none is set, instead just logs a WARN and lets the exhausted delivery fall through to AmqpRejectAndDontRequeueException semantics, relying on the queue’s own dead-letter configuration (or plain discard, if none is configured) to take it from there.

This is the Spring-side layer to reach for on top of, not instead of, the wait-queue retry topology RabbitMQ Reliability, Dead-Lettering and TTL builds from TTL and a DLX: the interceptor above retries in-process without a delay, while a wait-queue topology (or, on RabbitMQ 4.3 quorum queues, the broker-native delayed-retry feature that page also covers) adds a real, broker-held delay between attempts.

See Spring AMQP Reference — Resilience: Recovering from Errors and Broker Failures for the complete RetryInterceptorBuilder/MessageRecoverer API, including ImmediateRequeueAmqpException (the opposite of AmqpRejectAndDontRequeueException) and batch-listener retry caveats.

Request/reply: RPC, direct reply-to and @SendTo

AmqpTemplate#convertSendAndReceive is the Spring equivalent of the correlation-id/reply-to RPC pattern Consuming from RabbitMQ covers with direct reply-to at the AMQP level — it publishes the request, blocks (or, with AsyncRabbitTemplate, returns a CompletableFuture) up to replyTimeout (spring.rabbitmq.template.reply-timeout, 5 seconds by default) waiting for a correlated reply, and returns null on timeout:

@Service
public class PriceQuoteClient {

    private final RabbitTemplate rabbitTemplate;

    public PriceQuoteClient(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    public Quote requestQuote(QuoteRequest request) {
        Quote quote = (Quote) rabbitTemplate.convertSendAndReceive("rpc", "quotes.request", request);
        if (quote == null) {
            throw new QuoteTimeoutException(request);
        }
        return quote;
    }
}

useDirectReplyToContainer defaults to true since Spring AMQP 2.0, meaning RabbitTemplate uses RabbitMQ’s amq.rabbitmq.reply-to pseudo-queue automatically — exactly the mechanism Consuming from RabbitMQ describes — with no per-request queue declared or torn down, and no reply-listener bean to configure. A fixed reply queue is still occasionally useful (older brokers without direct reply-to support, or a reply pattern that must survive the requesting process restarting) and is configured with a named Queue bean plus setReplyAddress(…​) on the template and a listener container that feeds replies back into the template itself:

@Bean
public Queue repliesQueue() {
    return new Queue("orders.replies");
}

@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, Queue repliesQueue) {
    RabbitTemplate template = new RabbitTemplate(connectionFactory);
    template.setReplyAddress(repliesQueue.getName());
    template.setReplyTimeout(10_000);
    return template;
}

@Bean
public SimpleMessageListenerContainer replyListenerContainer(ConnectionFactory connectionFactory,
        Queue repliesQueue, RabbitTemplate rabbitTemplate) {
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
    container.setQueues(repliesQueue);
    container.setMessageListener(rabbitTemplate); // RabbitTemplate itself implements MessageListener here
    return container;
}

On the responding side, @SendTo turns an ordinary @RabbitListener method’s return value into the reply, without the handler ever touching replyTo/correlationId itself — the framework copies the correlation metadata from the inbound message onto the outbound reply automatically:

@RabbitListener(queues = "quotes.request")
@SendTo("quotes.reply")
public Quote quote(QuoteRequest request) {
    return priceQuoteService.quote(request);
}

See Spring AMQP Reference — Request/Reply Messaging for the complete sendAndReceive/convertSendAndReceive/AsyncRabbitTemplate API and fixed-reply- queue configuration, and Spring AMQP Reference — Reply Management for @SendTo and ReplyPostProcessor in full.

Transactions

RabbitTransactionManager is a PlatformTransactionManager implementation for RabbitMQ, used the same way any other Spring transaction manager is — @Transactional, or a TransactionTemplate — against a single ConnectionFactory. It does not provide XA transactions across RabbitMQ and a database in the same unit of work, only a local, RabbitMQ-only transaction (channel.txCommit()/txRollback() under the hood).

@Bean
public RabbitTransactionManager rabbitTransactionManager(ConnectionFactory connectionFactory) {
    return new RabbitTransactionManager(connectionFactory);
}

Transactions and publisher confirms are not meant to be combined — confirms already give a publisher a per-message acknowledgement that costs far less than a full broker-side transaction commit, so Publishing to RabbitMQ's confirm strategies remain the better default for publisher reliability; reach for RabbitTransactionManager where a listener’s processing genuinely needs to be atomic with its own re-publishing of other messages, not as a substitute for confirms.

See Spring AMQP Reference — Transactions for the complete transaction-synchronization behavior, including how a @RabbitListener container’s own channelTransacted setting interacts with a surrounding @Transactional boundary.

The delayed-message exchange plugin

RabbitMQ Reliability, Dead-Lettering and TTL already flags the community delayed-message exchange plugin as unmaintained on current RabbitMQ releases, in favor of RabbitMQ 4.3’s native delayed quorum-queue retries. Where the plugin is available, Spring AMQP’s support for it is a single boolean on the exchange bean — RabbitAdmin uses it to declare the exchange with type x-delayed-message and an x-delayed-type argument matching the exchange’s own type:

@Bean
public TopicExchange delayedOrdersExchange() {
    TopicExchange exchange = new TopicExchange("orders.delayed");
    exchange.setDelayed(true);
    return exchange;
}

The per-message delay itself is the x-delay header, in milliseconds, set through MessageProperties#setDelay or a MessagePostProcessor:

rabbitTemplate.convertAndSend("orders.delayed", "orders.eu.created", order,
        message -> {
            message.getMessageProperties().setDelay(15_000);
            return message;
        });

See Spring AMQP Reference — Delayed Message Exchange for this support in full, including getReceivedDelay() for reading the delay actually applied to an inbound message.

Multiple brokers

Talking to more than one broker (or the same cluster split across two vhosts, or a producer/consumer split to avoid the blocked-connection deadlock in the next section) from one Spring Boot application means excluding RabbitAutoConfiguration and wiring ConnectionFactory/RabbitAdmin/container factory beans by hand — Boot’s own auto-configuration only ever wires one of each. A SimpleRoutingConnectionFactory picks a target connection factory per operation by a routing key, and MultiRabbitListenerAnnotationBeanPostProcessor lets @RabbitListener methods each target a different RabbitListenerContainerFactory/broker instead of only the single default one:

@SpringBootApplication(exclude = RabbitAutoConfiguration.class)
public class Application {

    @Bean
    CachingConnectionFactory ordersConnectionFactory() {
        return new CachingConnectionFactory("orders-broker");
    }

    @Bean
    CachingConnectionFactory auditConnectionFactory() {
        return new CachingConnectionFactory("audit-broker");
    }

    @Bean
    SimpleRoutingConnectionFactory routingConnectionFactory(CachingConnectionFactory ordersConnectionFactory,
            CachingConnectionFactory auditConnectionFactory) {
        SimpleRoutingConnectionFactory routingFactory = new SimpleRoutingConnectionFactory();
        routingFactory.setDefaultTargetConnectionFactory(ordersConnectionFactory);
        routingFactory.setTargetConnectionFactories(
                Map.of("orders", ordersConnectionFactory, "audit", auditConnectionFactory));
        return routingFactory;
    }

    @Bean("orders-admin")
    RabbitAdmin ordersAdmin(CachingConnectionFactory ordersConnectionFactory) {
        return new RabbitAdmin(ordersConnectionFactory);
    }

    @Bean("audit-admin")
    RabbitAdmin auditAdmin(CachingConnectionFactory auditConnectionFactory) {
        return new RabbitAdmin(auditConnectionFactory);
    }
}

See Spring AMQP Reference — Multiple Broker (or Cluster) Support for the complete routing-connection-factory and @RabbitListener-per-broker setup, including how auto-declared queues get associated with the right target broker automatically.

Handling a blocked connection

RabbitMQ blocks a connection — stops reading from it — once a memory or disk resource alarm trips, the same flow-control mechanism Publishing to RabbitMQ covers at the AMQP level. Spring AMQP surfaces that as a ConnectionBlockedEvent/ConnectionUnblockedEvent ApplicationEvent pair published by the connection factory, or a com.rabbitmq.client.BlockedListener registered directly on the underlying Connection, either of which lets application code react (pause background publishing, raise an alert, shed load) instead of a producer thread simply stalling with no explanation:

@Component
public class BlockedConnectionMonitor {

    @EventListener
    public void onBlocked(ConnectionBlockedEvent event) {
        log.error("RabbitMQ connection blocked: {}", event.getReason());
    }

    @EventListener
    public void onUnblocked(ConnectionUnblockedEvent event) {
        log.info("RabbitMQ connection unblocked");
    }
}

A single CachingConnectionFactory shared between producers and consumers — the default under plain Boot auto-configuration — has a specific failure mode worth calling out: if producers are the ones triggering the resource alarm and consumers on the same connection are what would free up the broker again, a blocked connection can deadlock the whole application. RabbitTemplate#setUsePublisherConnection(true) has the template use a second, dedicated connection for publishing (transparently derived from the main connection factory) specifically to avoid that deadlock, without standing up a fully separate ConnectionFactory bean by hand.

See Spring AMQP Reference — Connection and Resource Management for the complete blocked-connection event contract and the "Using a Separate Connection" section this deadlock mitigation is documented under.

Streams: RabbitStreamTemplate and the AMQP 1.0 path

RabbitMQ Queue Types introduces RabbitMQ streams; Spring AMQP covers them from a separate module and API family from everything above, spring-rabbit-stream:

<dependency>
    <groupId>org.springframework.amqp</groupId>
    <artifactId>spring-rabbit-stream</artifactId>
</dependency>

RabbitStreamTemplate wraps the same Environment/Producer the stream protocol client uses directly on Publishing to RabbitMQ, returning a CompletableFuture<Boolean> per send rather than blocking, and Boot auto-configures both the Environment bean and, when spring.rabbitmq.listener.type=stream, a StreamRabbitListenerContainerFactory for @RabbitListener:

spring:
  rabbitmq:
    stream:
      name: orders-stream
    listener:
      type: stream
      stream:
        native-listener: true   # listener receives com.rabbitmq.stream.Message directly, not converted
@Bean
public Queue ordersStream() {
    return QueueBuilder.durable("orders-stream").stream().build();
}

@Component
public class OrderStreamPublisher {

    private final RabbitStreamTemplate streamTemplate;

    public OrderStreamPublisher(RabbitStreamTemplate streamTemplate) {
        this.streamTemplate = streamTemplate;
    }

    public void publish(Order order) {
        streamTemplate.convertAndSend(order);
    }
}

@Component
public class OrderStreamListener {

    @RabbitListener(queues = "orders-stream", containerFactory = "rabbitListenerContainerFactory")
    public void onOrder(Order order) {
        process(order);
    }
}

See Spring AMQP Reference — Using the RabbitMQ Stream Plugin for the complete RabbitStreamTemplate/StreamListenerContainer API, offset tracking (the Spring equivalent of Consuming from RabbitMQ's stream offset-tracking section) and super-stream support.

The AMQP 1.0 path: spring-rabbitmq-client

RabbitMQ and AMQP 1.0 covers the protocol this module speaks — sessions and links instead of channels, credit-based flow control instead of prefetch, settlement outcomes instead of ack/nack. spring-rabbitmq-client (new in Spring AMQP 4.0, RabbitMQ-specific — not the same module as the broker-agnostic spring-amqp-client AMQP 1.0 support new in 4.1) mirrors the classic spring-rabbit API one level down, with every send returning a CompletableFuture instead of blocking:

Concept Classic (spring-rabbit, AMQP 0-9-1) AMQP 1.0 (spring-rabbitmq-client)

Connection factory

org.springframework.amqp.rabbit.connection.ConnectionFactory

AmqpConnectionFactory (SingleAmqpConnectionFactory)

Template

RabbitTemplate — synchronous

RabbitAmqpTemplate — every method returns a CompletableFuture

Admin

RabbitAdmin

RabbitAmqpAdmin — same Exchange/Queue/Binding/Declarables beans

Listener container

SimpleMessageListenerContainer / DirectMessageListenerContainer

RabbitAmqpListenerContainer, via RabbitAmqpListenerContainerFactory

@Bean
Environment environment() {
    return new AmqpEnvironmentBuilder().build();
}

@Bean
AmqpConnectionFactory amqpConnectionFactory(Environment environment) {
    return new SingleAmqpConnectionFactory(environment);
}

@Bean
RabbitAmqpAdmin rabbitAmqpAdmin(AmqpConnectionFactory amqpConnectionFactory) {
    return new RabbitAmqpAdmin(amqpConnectionFactory);
}

@Bean
RabbitAmqpTemplate rabbitAmqpTemplate(AmqpConnectionFactory amqpConnectionFactory) {
    return new RabbitAmqpTemplate(amqpConnectionFactory);
}

@Bean(RabbitListenerAnnotationBeanPostProcessor.DEFAULT_RABBIT_LISTENER_CONTAINER_FACTORY_BEAN_NAME)
RabbitAmqpListenerContainerFactory rabbitAmqpListenerContainerFactory(AmqpConnectionFactory amqpConnectionFactory) {
    return new RabbitAmqpListenerContainerFactory(amqpConnectionFactory);
}

@RabbitListener on this path accepts AmqpAcknowledgment (settlement — acknowledge(), acknowledge(Status.REQUEUE), and so on) or the native Consumer.Context directly, and its ackMode attribute takes an AcknowledgeMode the same way the container-level property does on the classic path:

@RabbitListener(queues = "orders", ackMode = "#{T(org.springframework.amqp.core.AcknowledgeMode).MANUAL}")
public void onOrder(Order order, AmqpAcknowledgment acknowledgment) {
    try {
        process(order);
        acknowledgment.acknowledge();               // settlement outcome: accepted
    } catch (RecoverableException e) {
        acknowledgment.acknowledge(AmqpAcknowledgment.Status.REQUEUE); // settlement outcome: released
    }
}

See Spring AMQP Reference — RabbitMQ AMQP 1.0 Support for the complete Environment/AmqpConnectionFactory/RabbitAmqpTemplate/ RabbitAmqpListenerContainerFactory API, and Spring AMQP Reference — Generic AMQP 1.0 Support for the newer, broker-agnostic spring-amqp-client/@AmqpListener module this page does not otherwise cover, since this section’s scope is RabbitMQ specifically.