skip to content

What is RepublishMessageRecoverer and why prefer it over the default RejectAndDontRequeueRecoverer?

level: seniorimportance: should knowfreq 60%

answer

  1. MessageRecoverer = post-retry-exhaustion hook
  2. default RejectAndDontRequeueRecoverer relies on broker DLX
  3. Republish adds x-exception-message / -stacktrace / x-original-*
  4. RepublishMessageRecovererWithConfirms for guaranteed delivery
  5. override additionalHeaders() for custom metadata

basics

~20 s

It'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 s

RepublishMessageRecoverer 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
java
@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

for a junior

Know it's the thing that sends exhausted messages to a dead-letter queue with error info.

for a middle

Know it runs after retries are exhausted and adds exception headers, unlike the default recoverer.

for a senior

Explain the diagnostic headers, the confirms variant, and how it composes with the retry interceptor.

for a principal

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

context