java 60 lines · 9 steps

Dead-letter recovery with Spring AMQP

A Spring component consumes dead-lettered orders, tracks retry attempts, and either re-queues with backoff or escalates.

Explained by highlit
1@Component
2public class OrderRecoveryHandler {
3 
4 private final RabbitTemplate rabbitTemplate;
5 private final RecoveryAttemptRepository attempts;
6 private final AlertService alertService;
7 
8 public OrderRecoveryHandler(RabbitTemplate rabbitTemplate,
9 RecoveryAttemptRepository attempts,
10 AlertService alertService) {
11 this.rabbitTemplate = rabbitTemplate;
12 this.attempts = attempts;
13 this.alertService = alertService;
14 }
15 
16 @RabbitListener(queues = "orders.payment.dlq")
17 public void onPaymentFailure(FailedOrderMessage message,
18 @Header("x-death") List<Map<String, ?>> death) {
19 recover(message, RecoverySource.PAYMENT, deathCount(death));
20 }
21 
22 @RabbitListener(queues = "orders.fulfillment.dlq")
23 public void onFulfillmentFailure(FailedOrderMessage message,
24 @Header("x-death") List<Map<String, ?>> death) {
25 recover(message, RecoverySource.FULFILLMENT, deathCount(death));
26 }
27 
28 private void recover(FailedOrderMessage message, RecoverySource source, long deaths) {
29 RecoveryAttempt attempt = attempts.findByOrderId(message.orderId())
30 .orElseGet(() -> new RecoveryAttempt(message.orderId()));
31 attempt.record(source, message.reason());
32 
33 if (deaths >= 3 || attempt.hasFailedIn(RecoverySource.PAYMENT, RecoverySource.FULFILLMENT)) {
34 attempt.escalate();
35 attempts.save(attempt);
36 alertService.pageOnCall(message.orderId(), attempt.summary());
37 return;
38 }
39 
40 attempts.save(attempt);
41 rabbitTemplate.convertAndSend(
42 "orders.retry.exchange",
43 source.routingKey(),
44 message,
45 m -> {
46 m.getMessageProperties().setDelayLong(backoffMillis(deaths));
47 return m;
48 });
49 }
50 
51 private long deathCount(List<Map<String, ?>> death) {
52 return death == null || death.isEmpty()
53 ? 1L
54 : ((Number) death.get(0).getOrDefault("count", 1L)).longValue();
55 }
56 
57 private long backoffMillis(long deaths) {
58 return Math.min(Duration.ofMinutes(5).toMillis(), 2000L * (1L << deaths));
59 }
60}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Dead-letter queues plus a persisted attempt record let you retry failures safely without losing track of history.
  2. 2Capping exponential backoff prevents retry delays from growing unbounded while still spacing out attempts.
  3. 3Escalation rules based on death count and cross-source failure stop poison messages from retrying forever.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Dead-letter recovery with Spring AMQP — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code