java
45 lines · 6 steps
Manual RabbitMQ acks in a Spring listener
A Spring AMQP listener that explicitly acks, rejects, or requeues order messages based on how processing fails.
Explained by
highlit
1package com.example.orders.messaging;
2
3import com.example.orders.dto.OrderEvent;
4import com.example.orders.service.OrderProcessingService;
5import com.rabbitmq.client.Channel;
6import org.slf4j.Logger;
7import org.slf4j.LoggerFactory;
8import org.springframework.amqp.rabbit.annotation.RabbitListener;
9import org.springframework.amqp.support.AmqpHeaders;
10import org.springframework.messaging.handler.annotation.Header;
11import org.springframework.messaging.handler.annotation.Payload;
12import org.springframework.stereotype.Component;
13
14import java.io.IOException;
15
16@Component
17public class OrderEventListener {
18
19 private static final Logger log = LoggerFactory.getLogger(OrderEventListener.class);
20
21 private final OrderProcessingService orderProcessingService;
22
23 public OrderEventListener(OrderProcessingService orderProcessingService) {
24 this.orderProcessingService = orderProcessingService;
25 }
26
27 @RabbitListener(queues = "orders.created", ackMode = "MANUAL")
28 public void onOrderCreated(
29 @Payload OrderEvent event,
30 Channel channel,
31 @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag,
32 @Header(name = AmqpHeaders.REDELIVERED, defaultValue = "false") boolean redelivered) throws IOException {
33
34 try {
35 orderProcessingService.process(event);
36 channel.basicAck(deliveryTag, false);
37 } catch (IllegalStateException permanent) {
38 log.warn("Rejecting unprocessable order {}: {}", event.orderId(), permanent.getMessage());
39 channel.basicReject(deliveryTag, false);
40 } catch (Exception transientFailure) {
41 log.error("Failed to process order {} (redelivered={})", event.orderId(), redelivered, transientFailure);
42 channel.basicNack(deliveryTag, false, !redelivered);
43 }
44 }
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Manual ack mode lets you distinguish permanent failures from transient ones instead of blindly requeuing everything.
- 2Rejecting with requeue=false hands poison messages to a dead-letter queue rather than looping forever.
- 3Tracking the redelivered flag prevents infinite retry loops by requeuing only on the first transient failure.
Related explainers
rust
use serde::Deserialize; #[derive(Debug, Deserialize)] #[serde(untagged)]
Parsing flexible JSON shapes with serde
deserialization
enums
json
Intermediate
6 steps
ruby
require "shellwords" require "open3" module Backup
Building safe shell commands in Ruby
shell-out
subprocess
command-injection
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
java
package com.acme.billing.config; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties;
Feature-flagged beans with Spring @ConditionalOnProperty
feature-flags
conditional-beans
strategy-pattern
Intermediate
5 steps
rust
use chrono::{Duration, NaiveDate}; #[derive(Debug)] pub struct DateRange {
Parsing and iterating date ranges in Rust
error-handling
iterators
parsing
Intermediate
7 steps
java
public static Map<String, String> parseCookieHeader(String header) { Map<String, String> cookies = new LinkedHashMap<>(); if (header == null || header.isBlank()) { return cookies;
Parsing an HTTP Cookie header in Java
string-parsing
http
url-decoding
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/manual-rabbitmq-acks-in-a-spring-listener-explained-java-0cc0/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.