A dead letter queue (DLQ) is a special queue that holds messages that cannot be processed successfully. It helps keep your main queue clean and lets you inspect or fix problem messages later.
Dead letter queues in Spring Boot
@Configuration public class RabbitConfig { public static final String MAIN_QUEUE = "main.queue"; public static final String DEAD_LETTER_QUEUE = "dead.letter.queue"; public static final String EXCHANGE = "main.exchange"; @Bean Queue mainQueue() { return QueueBuilder.durable(MAIN_QUEUE) .withArgument("x-dead-letter-exchange", "") .withArgument("x-dead-letter-routing-key", DEAD_LETTER_QUEUE) .build(); } @Bean Queue deadLetterQueue() { return QueueBuilder.durable(DEAD_LETTER_QUEUE).build(); } @Bean DirectExchange exchange() { return new DirectExchange(EXCHANGE); } @Bean Binding mainBinding() { return BindingBuilder.bind(mainQueue()).to(exchange()).with(MAIN_QUEUE); } @Bean Binding deadLetterBinding() { return BindingBuilder.bind(deadLetterQueue()).to(new DirectExchange("")).with(DEAD_LETTER_QUEUE); } }
The x-dead-letter-exchange argument tells RabbitMQ where to send messages that are rejected or expire.
The x-dead-letter-routing-key sets the routing key for the dead letter queue.
@Bean
Queue mainQueue() {
return QueueBuilder.durable("main.queue")
.withArgument("x-dead-letter-exchange", "")
.withArgument("x-dead-letter-routing-key", "dead.letter.queue")
.build();
}@Bean
Queue deadLetterQueue() {
return QueueBuilder.durable("dead.letter.queue").build();
}@Bean
Binding deadLetterBinding() {
return BindingBuilder.bind(deadLetterQueue()).to(new DirectExchange("")).with("dead.letter.queue");
}This Spring Boot app configures a main queue with a dead letter queue. The listener throws an error for messages containing "fail". Those messages go to the dead letter queue and are logged separately.
package com.example.demo; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } } @Configuration class RabbitConfig { public static final String MAIN_QUEUE = "main.queue"; public static final String DEAD_LETTER_QUEUE = "dead.letter.queue"; public static final String EXCHANGE = "main.exchange"; @Bean Queue mainQueue() { return QueueBuilder.durable(MAIN_QUEUE) .withArgument("x-dead-letter-exchange", "") .withArgument("x-dead-letter-routing-key", DEAD_LETTER_QUEUE) .build(); } @Bean Queue deadLetterQueue() { return QueueBuilder.durable(DEAD_LETTER_QUEUE).build(); } @Bean DirectExchange exchange() { return new DirectExchange(EXCHANGE); } @Bean Binding mainBinding() { return BindingBuilder.bind(mainQueue()).to(exchange()).with(MAIN_QUEUE); } @Bean Binding deadLetterBinding() { return BindingBuilder.bind(deadLetterQueue()).to(new DirectExchange("")).with(DEAD_LETTER_QUEUE); } } @Component class MessageListener { @RabbitListener(queues = RabbitConfig.MAIN_QUEUE) public void listen(String message) { System.out.println("Received message: " + message); if (message.contains("fail")) { System.out.println("Message processing failed, rejecting message."); throw new RuntimeException("Processing failed"); } System.out.println("Message processed successfully."); } } @Component class DeadLetterListener { @RabbitListener(queues = RabbitConfig.DEAD_LETTER_QUEUE) public void listenDeadLetter(String message) { System.out.println("Dead letter received: " + message); } }
Time complexity: Message routing is O(1) in RabbitMQ.
Space complexity: Dead letter queue stores failed messages separately, so plan storage accordingly.
Common mistake: Forgetting to set both x-dead-letter-exchange and x-dead-letter-routing-key causes messages not to route to DLQ.
Use dead letter queues when you want to isolate and inspect failed messages instead of losing or blocking them.
Dead letter queues hold messages that fail processing to keep main queues clean.
Configure DLQ by setting queue arguments x-dead-letter-exchange and x-dead-letter-routing-key.
Use DLQs to analyze, retry, or fix problem messages without blocking your system.
Practice
Solution
Step 1: Understand message failure handling
Dead Letter Queues are designed to store messages that cannot be processed successfully.Step 2: Identify the role of DLQ in queue management
DLQs keep the main queue clean by isolating failed messages, allowing the system to continue processing other messages.Final Answer:
To hold messages that failed processing and prevent blocking the main queue -> Option BQuick Check:
DLQ purpose = hold failed messages [OK]
- Thinking DLQ speeds up processing by duplicating messages
- Confusing DLQ with message deletion
- Assuming DLQ encrypts messages
Solution
Step 1: Identify DLQ configuration parameters
Dead Letter Queues require setting the queue argumentsx-dead-letter-exchangeandx-dead-letter-routing-keyto route failed messages.Step 2: Eliminate incorrect options
x-message-ttlcontrols message time-to-live, not DLQ; no@EnableDeadLetterQueueannotation exists; no direct propertyspring.rabbitmq.dlq.enabled.Final Answer:
Set queue argumentx-dead-letter-exchangeandx-dead-letter-routing-key-> Option AQuick Check:
DLQ config = x-dead-letter-exchange + routing key [OK]
- Confusing TTL with DLQ configuration
- Looking for non-existent annotations
- Expecting simple property toggle for DLQ
@RabbitListener(queues = "mainQueue")
public void listen(String msg) {
if(msg.contains("fail")) throw new RuntimeException("Fail");
System.out.println("Processed: " + msg);
}
Assuming mainQueue has a DLQ configured, what is the expected behavior?Solution
Step 1: Understand exception handling in RabbitMQ listener
When an exception occurs, the message is rejected and, if DLQ is configured, routed there.Step 2: Confirm system behavior with DLQ
The failed message goes to the Dead Letter Queue, allowing the main queue to continue processing other messages.Final Answer:
The failed message is sent to the Dead Letter Queue and processing continues -> Option DQuick Check:
Exception triggers DLQ routing [OK]
- Assuming app crashes on exception
- Thinking message is lost silently
- Believing infinite retries without DLQ
Solution
Step 1: Check DLQ configuration completeness
If the main queue lacks thex-dead-letter-exchangeargument, failed messages have no route to DLQ.Step 2: Evaluate other options
Listener throwing exceptions is needed but not sufficient; DLQ name length and empty payload do not prevent DLQ routing.Final Answer:
The main queue is missing thex-dead-letter-exchangeargument -> Option CQuick Check:
Missing DLX argument blocks DLQ routing [OK]
- Ignoring missing DLX argument
- Blaming listener behavior alone
- Worrying about DLQ name length unnecessarily
Solution
Step 1: Understand retry and DLQ roles
Retries allow multiple attempts to process a message before considering it failed.Step 2: Combine retry interceptor with DLQ
Configure a retry interceptor on the listener to retry processing; if all retries fail, the message is routed to the DLQ.Step 3: Eliminate incorrect options
Sending all messages directly to DLQ skips normal processing; disabling DLQ loses failed message tracking; RabbitMQ does not retry automatically without app logic.Final Answer:
Use a retry interceptor on the listener and configure DLQ for messages failing all retries -> Option AQuick Check:
Retry interceptor + DLQ = robust failure handling [OK]
- Sending all messages directly to DLQ
- Disabling DLQ and expecting retries only
- Assuming RabbitMQ retries automatically
Ready to prove it?
Pass the test and this pattern moves unknown
