What is RepublishMessageRecoverer and why prefer it over the default RejectAndDontRequeueRecoverer?
answer
- MessageRecoverer = post-retry-exhaustion hook
- default RejectAndDontRequeueRecoverer relies on broker DLX
- Republish adds x-exception-message / -stacktrace / x-original-*
- RepublishMessageRecovererWithConfirms for guaranteed delivery
- override additionalHeaders() for custom metadata
basics
~20 sIt's a MessageRecoverer that, after retries are exhausted, republishes the failed message to a dead-letter exchange/queue and adds headers with the exception message and stack trace. Unlike the default recoverer, you get diagnostic context in the DLQ.
solid answer
~40 sRepublishMessageRecoverer is a MessageRecoverer plugged into the listener's retry interceptor. When in-memory retries are exhausted, instead of just rejecting-without-requeue (what the default RejectAndDontRequeueRecoverer does, relying on the broker's DLX), it *actively republishes* the message to an exchange/routing key you specify and enriches it with headers: x-exception-message, x-exception-stacktrace, x-original-exchange, x-original-routingKey. That means the DLQ message carries *why* it failed — invaluable for triage — which broker-level dead-lettering doesn't give you. It also acks the original delivery after republishing. You'd prefer it when you want failure diagnostics and explicit control over the dead-letter destination, rather than relying solely on the queue's x-dead-letter-exchange argument. You can subclass it (e.g. RepublishMessageRecovererWithConfirms) or override additionalHeaders() for custom metadata.
code
java · 20 lines@Bean
MessageRecoverer messageRecoverer(RabbitTemplate template) {
return new RepublishMessageRecoverer(template, "orders.dlx", "orders.dead");
}
@Bean
SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory cf,
SimpleRabbitListenerContainerFactoryConfigurer configurer,
MessageRecoverer recoverer) {
var factory = new SimpleRabbitListenerContainerFactory();
configurer.configure(factory, cf);
factory.setAdviceChain(
RetryInterceptorBuilder.stateless()
.maxAttempts(3)
.backOffOptions(1000, 2.0, 10_000)
.recoverer(recoverer)
.build());
return factory;
}go deeper
Know it's the thing that sends exhausted messages to a dead-letter queue with error info.
Know it runs after retries are exhausted and adds exception headers, unlike the default recoverer.
Explain the diagnostic headers, the confirms variant, and how it composes with the retry interceptor.
Decide republish-with-confirms vs broker DLX per durability requirements; standardize DLQ header conventions for triage/tooling.
**Context — MessageRecoverer:** Spring AMQP's listener retry is driven by a `RetryOperationsInterceptor` (backed by a `RetryTemplate`). When all retry attempts are **exhausted**, the interceptor calls a **`MessageRecoverer`** to decide the message's final fate. The recoverer is the 'what do we do with a message we've given up on' hook. **The default: `RejectAndDontRequeueRecoverer`.** It simply throws `AmqpRejectAndDontRequeueException`, causing the container to nack with requeue=false. If the queue has an `x-dead-letter-exchange`, the broker dead-letters it; otherwise it's dropped. Simple, but the DLQ message has **no information about why it failed** — just an `x-death` header saying 'rejected'. **`RepublishMessageRecoverer`:** A richer recoverer that, on exhaustion, **republishes** the message itself (via an `AmqpTemplate`/`RabbitTemplate`) to an exchange + routing key you configure, then acks the original. Crucially, it **adds diagnostic headers** to the republished message: - `x-exception-message` — the exception's message. - `x-exception-stacktrace` — the full stack trace (truncated to a configurable frame-size limit; default caps the stacktrace length). - `x-original-exchange` — the exchange the message first arrived on. - `x-original-routingKey` — its original routing key. So when an on-call engineer opens the DLQ, each message *self-documents* its failure cause. That's the main reason to prefer it. **Construction:** ```java new RepublishMessageRecoverer(rabbitTemplate, "orders.dlx", "orders.dead"); ``` If you pass only an exchange, the original routing key is reused. You can call `.errorRoutingKeyPrefix(...)` to prefix DLQ routing keys, or `.addRecoverType(...)`. **Delivery-mode & confirms nuance:** Because it *republishes* (a fresh publish), you don't automatically get the guarantee that the message landed in the DLQ. Spring provides **`RepublishMessageRecovererWithConfirms`**, which waits for publisher confirms (or confirms+returns) before acking the original delivery — closing the window where a republish could be lost. Use it when losing dead-lettered messages is unacceptable. **Customizing headers:** Override `additionalHeaders(Message message, Throwable cause)` to attach your own metadata (retry count, tenant id, correlation id). **Wiring it up:** You attach the recoverer to the retry interceptor and set that as the container factory's advice chain: ```java RetryInterceptorBuilder.stateless() .maxAttempts(3) .backOffOptions(1000, 2.0, 10000) .recoverer(new RepublishMessageRecoverer(template, "orders.dlx", "orders.dead")) .build(); ``` With Spring Boot, you can also configure retry via `spring.rabbitmq.listener.simple.retry.*` properties, and define a `MessageRecoverer` bean — Boot auto-wires it into the interceptor. **Republish vs broker dead-lettering — how they relate:** They solve the same problem two ways. Broker DLX (via `x-dead-letter-exchange`) is passive: the broker moves rejected/expired messages. RepublishMessageRecoverer is active: your app publishes to a target of its choosing with rich headers. You can use *either or both*; if you use RepublishMessageRecoverer you often don't need the queue's DLX argument for the retry path (though you might still keep it for TTL/maxlen cases). A subtle point: the republished message is a **new** message, so its `x-death` history from the original isn't carried the same way — the diagnostic headers replace that role. **Gotchas:** - Ensure the target exchange/queue for the recoverer actually exists and is bound, or the republish is unroutable and lost (again, confirms+returns help detect this). - The stack-trace header can be large; there's a max-stack-trace-length cap to avoid oversized frames — very deep traces are truncated. - The recoverer runs on exhaustion of *app-level* retries, not on the first failure, so it composes with the retry interceptor's backoff.
- Why might you choose RepublishMessageRecovererWithConfirms over the plain recoverer?The plain recoverer acks the original delivery right after republishing; if the republish is lost (e.g. unroutable, or broker crash before persistence) the message vanishes. The confirms variant waits for a publisher confirm (and return) before acking, so a failed republish doesn't silently lose the message.
- What extra information does RepublishMessageRecoverer put in the DLQ message that broker dead-lettering does not?It adds headers x-exception-message, x-exception-stacktrace, x-original-exchange, and x-original-routingKey, so the DLQ message self-documents the failure cause. Broker DLX only adds the x-death routing history with a coarse reason (rejected/expired/maxlen).
saying these in an interview costs you the question
- Saying RepublishMessageRecoverer runs on every failure rather than after retry exhaustion
- Claiming it guarantees delivery to the DLQ without confirms
- Confusing it with broker-level x-dead-letter-exchange dead-lettering