Implementing Message Confirmation in Spring Boot with RabbitMQ: Configuration, Callbacks, and Common Pitfalls
This article explains how to set up Spring Boot and RabbitMQ message confirmation, covering environment preparation, publisher and consumer callback implementations, acknowledgment methods, testing procedures, and practical pitfalls such as missed acknowledgments, infinite redelivery loops, and duplicate consumption.
Recently the department encouraged everyone to organize technical sharing sessions to activate the company's technical atmosphere, which the author sees as a KPI‑driven activity, but acknowledges that genuine knowledge exchange is valuable for personal growth.
The author volunteered to present a talk on springboot + rabbitmq focusing on implementing message confirmation mechanisms and sharing practical pitfalls.
Using RabbitMQ extends the business call chain, achieving system decoupling but also increasing the risk of message loss. Typical loss scenarios include producer‑to‑broker failures, broker crashes, and consumer‑to‑broker failures.
1. Environment Preparation
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
spring.rabbitmq.publisher-confirms=true
# Enable publisher returns
spring.rabbitmq.publisher-returns=true
# Manual ack for consumer
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# Enable retry
spring.rabbitmq.listener.simple.retry.enabled=true1.3 Define Exchange and Queue
Define a confirmTestExchange and a confirm_test_queue , then bind the queue to the exchange.
@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);
}
}RabbitMQ's message confirmation consists of two parts: publisher confirm and consumer ack.
2. Publisher Confirmation
Publisher confirmation ensures that the producer's message reaches the broker ( confirmCallback ) and that the broker successfully routes the message to the target queue ( returnCallback ).
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 sending exception!");
} else {
log.info("Publisher received confirm, correlationData={}, ack={}, cause={}", correlationData.getId(), ack, cause);
}
}
}The confirm() method receives correlationData (unique ID), ack (true if broker accepted), and cause (failure reason).
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);
}
}The returnedMessage() method is invoked when a message cannot be routed to a queue, providing details such as replyCode , replyText , exchange , and routingKey .
2.3 Sending a message 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. Consumer Confirmation
Consumer acknowledgment is simpler: only an ack is required. Methods annotated with @RabbitHandler must accept a Channel and a Message to perform manual acks.
@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);
}
}
}
}Three acknowledgment methods are discussed:
3.1 basicAck
Confirms successful processing; the broker removes the message.
void basicAck(long deliveryTag, boolean multiple)3.2 basicNack
Indicates failure; can requeue the message for another attempt.
void basicNack(long deliveryTag, boolean multiple, boolean requeue)3.3 basicReject
Rejects a message without batch support; optionally requeues.
void basicReject(long deliveryTag, boolean requeue)4. Testing
After sending messages, the author observed successful publisher callbacks and consumer processing. Network traffic captured with Wireshark showed additional ack frames in the AMQP protocol.
5. Common Pitfalls
5.1 Forgetting to acknowledge
If the consumer does not call channel.basicAck , the message remains in the queue and may be consumed repeatedly.
5.2 Infinite redelivery
When an exception occurs after the business logic, the message can be nacked and requeued indefinitely, causing a processing loop and CPU overload.
@RabbitHandler
public void processHandler(String msg, Channel channel, Message message) throws IOException {
try {
log.info("Consumer 2 received: {}", msg);
int a = 1 / 0; // intentional error
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}The solution is to acknowledge the message first, then republish it to the tail of the queue, or limit retry attempts and persist failed messages to MySQL for manual handling.
5.3 Duplicate consumption
Ensuring idempotency can be achieved by persisting processed message IDs in MySQL or Redis and checking uniqueness before processing.
Finally, the author invites readers to like the post, claim a collection of technical e‑books by replying with "666", and join a technical discussion group.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.