Mastering Spring Boot & RabbitMQ Message Confirmation: Tips & Pitfalls

This article walks through implementing message confirmation in a Spring Boot application using RabbitMQ, covering environment setup, configuration, producer and consumer callback implementations, acknowledgment methods, common pitfalls such as missed acks and infinite redelivery, and practical debugging and retry strategies to ensure reliable messaging.

macrozheng
macrozheng
macrozheng
Mastering Spring Boot & RabbitMQ Message Confirmation: Tips & Pitfalls

This guide demonstrates how to enable and use message confirmation in a Spring Boot application that integrates with RabbitMQ, providing step‑by‑step configuration, code examples, and troubleshooting tips.

1. Preparation Environment

1.1 Add RabbitMQ dependency

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

1.2 Configure application.properties

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# Enable publisher confirms and returns
spring.rabbitmq.publisher-confirms=true
spring.rabbitmq.publisher-returns=true
# Manual ack for consumers
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# Enable retry
spring.rabbitmq.listener.simple.retry.enabled=true

1.3 Define Exchange and Queue

@Configuration
public class QueueConfig {
    @Bean(name = "confirmTestQueue")
    public Queue confirmTestQueue() {
        return new Queue("confirm_test_queue", true, false, false);
    }
    @Bean(name = "confirmTestExchange")
    public FanoutExchange confirmTestExchange() {
        return new FanoutExchange("confirmTestExchange");
    }
    @Bean
    public Binding confirmTestFanoutExchangeAndQueue(@Qualifier("confirmTestExchange") FanoutExchange confirmTestExchange,
                                                    @Qualifier("confirmTestQueue") Queue confirmTestQueue) {
        return BindingBuilder.bind(confirmTestQueue).to(confirmTestExchange);
    }
}

2. Message Send Confirmation

RabbitMQ provides two callbacks: ConfirmCallback for producer‑side confirmation and ReturnCallback for undeliverable messages.

2.1 ConfirmCallback implementation

@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            log.error("Message send failed!");
        } else {
            log.info("Publisher received ack, correlationData={}, ack={}, cause={}",
                     correlationData.getId(), ack, cause);
        }
    }
}

2.2 ReturnCallback implementation

@Slf4j
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnCallback {
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText,
                               String exchange, String routingKey) {
        log.info("returnedMessage => replyCode={}, replyText={}, exchange={}, routingKey={}",
                 replyCode, replyText, exchange, routingKey);
    }
}

2.3 Sending messages with callbacks

@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ConfirmCallbackService confirmCallbackService;
@Autowired
private ReturnCallbackService returnCallbackService;

public void sendMessage(String exchange, String routingKey, Object msg) {
    rabbitTemplate.setMandatory(true);
    rabbitTemplate.setConfirmCallback(confirmCallbackService);
    rabbitTemplate.setReturnCallback(returnCallbackService);
    rabbitTemplate.convertAndSend(exchange, routingKey, msg,
        message -> {
            message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
            return message;
        },
        new CorrelationData(UUID.randomUUID().toString()));
}

3. Message Receive Confirmation

Consumer side uses manual acknowledgments. The handler must receive the Channel and Message objects.

@Slf4j
@Component
@RabbitListener(queues = "confirm_test_queue")
public class ReceiverMessage1 {
    @RabbitHandler
    public void processHandler(String msg, Channel channel, Message message) throws IOException {
        try {
            log.info("Received message: {}", msg);
            // TODO business logic
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            if (message.getMessageProperties().getRedelivered()) {
                log.error("Message repeatedly failed, reject without requeue");
                channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
            } else {
                log.error("Message will be requeued for another attempt");
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            }
        }
    }
}

3.1 Acknowledgment methods

basicAck : confirms successful processing; the broker removes the message.

basicNack : negative ack, optionally requeues the message for redelivery.

basicReject : rejects a single message; can also requeue.

4. Testing

After sending a test message, the publisher receives a confirm callback and the consumer logs successful processing. Network traces (e.g., with Wireshark) show the AMQP ack flow.

5. Common Pitfalls and Solutions

5.1 Forgetting to ack

If the consumer does not call channel.basicAck, the message remains unacknowledged and will be redelivered repeatedly.

5.2 Infinite redelivery loops

When an exception occurs after a failed ack, using basicNack with requeue can cause the same message to be placed at the head of the queue, leading to a tight loop. A workaround is to ack the problematic message first, then republish it to the tail of the queue.

channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
channel.basicPublish(message.getMessageProperties().getReceivedExchange(),
                    message.getMessageProperties().getReceivedRoutingKey(),
                    MessageProperties.PERSISTENT_TEXT_PLAIN,
                    JSON.toJSONBytes(msg));

Further improvement includes limiting retry attempts, persisting failed messages to MySQL, and triggering alerts for manual handling.

5.3 Duplicate consumption

Ensuring idempotency may require persisting a unique message identifier in MySQL or Redis and checking it before processing.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaSpring BootRabbitMQMessage Confirmation
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.