RabbitMQ is the most widely used open-source message broker โ a name you will meet in many backend teams. In this chapter you will run one and build everything from the previous chapter for real: a Spring Boot producer, a queue on the broker, and a consumer โ then watch work wait safely while the application is down.
Add a rabbitmq service to a compose.yaml, exactly like you added PostgreSQL in Week 11:
services:
rabbitmq:
image: rabbitmq:4-management
ports:
- "5672:5672" # AMQP: applications connect here
- "15672:15672" # management UI in the browser
environment:
RABBITMQ_DEFAULT_USER: myuser
RABBITMQ_DEFAULT_PASS: mysecret
<aside> ๐ก
The -management tag incudes a web UI. RabbitMQ listens on two ports: 5672 for applications (the AMQP protocol) and 15672 for humans (the management UI).
</aside>
<aside> โจ๏ธ
Hands on: run docker compose up -d, open http://localhost:15672 and log in with myuser / mysecret. You are looking at an empty broker.
</aside>
<aside> โ ๏ธ
Hard-coded credentials are fine for a local lab. In a real deployment they are injected through environment variables, exactly as you did with database credentials in Week 11.
</aside>
Create a fresh Spring Boot project (Spring Web) or reuse a sandbox one, and add the AMQP starter:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
Tell Spring where the broker is, in application.yaml:
spring:
rabbitmq:
host: localhost
port: 5672
username: myuser
password: mysecret
Declare the queue as a bean โ Spring creates it on the broker at startup if it does not exist:
package net.hackyourfuture.tasks.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MessagingConfiguration {
public static final String TASKS_QUEUE = "tasks";
@Bean
public Queue tasksQueue() {
return new Queue(TASKS_QUEUE);
}
}
<aside> โ ๏ธ
Import org.springframework.amqp.core.Queue โ not java.util.Queue. Picking the wrong import here is the classic mistake of this lab.
</aside>
Producing is one method call on RabbitTemplate, which Spring auto-configures for you:
@RestController
@RequestMapping("/tasks")
public class TaskController {
private final RabbitTemplate rabbitTemplate;
public TaskController(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@PostMapping
public ResponseEntity<Void> createTask(@RequestBody String task) {
rabbitTemplate.convertAndSend(MessagingConfiguration.TASKS_QUEUE, task);
return ResponseEntity.accepted().build();
}
}
<aside> ๐ก
202 Accepted is the HTTP status that says "received, will be processed later" โ the honest answer for asynchronous work (remember the status codes from Week 4).
</aside>
<aside> โจ๏ธ
Hands on: start the application and send a few tasks:
curl -X POST localhost:8080/tasks -H "Content-Type: text/plain" -d "task-1"
There is no consumer yet โ so open the management UI, go to Queues โ tasks, and see your messages sitting there as Ready. Work, waiting patiently.
</aside>
A consumer is one annotated method. Spring calls it for every message that arrives:
@Component
@Slf4j
public class TaskWorker {
@RabbitListener(queues = MessagingConfiguration.TASKS_QUEUE)
public void handleTask(String task) throws InterruptedException {
log.info("Processing: {}", task);
Thread.sleep(3000); // simulate slow work
log.info("Done: {}", task);
}
}
Restart the application and watch the logs: the messages that were waiting in the queue are processed one by one.
<aside> ๐
You just built a complete messaging system: producer โ queue โ consumer, with real work surviving in a real broker.
</aside>
tasks โ Publish message, and publish a payload like task-99 three times. The queue shows Ready: 3 โ the work is waiting while no application is even running.<aside> โ
This is exactly what @Async could not do: the work survived the application being down. The broker, not your app, is now the safe place where work waits.
</aside>
Under the hood, the listener acknowledges each message automatically when your method returns normally. If the method throws, or the application crashes mid-task, there is no acknowledgement โ and the broker delivers the message again.
That guarantee is called at-least-once delivery, and it has a famous consequence: the same message can be processed twice (for example: the work finished, but the app crashed just before acknowledging). Consumers should therefore be idempotent โ processing the same message twice gives the same result as once, usually by checking "is this already done?" before doing it. You will build exactly that on the Practice page.
<aside> ๐ญ
In RabbitMQ, messages actually travel through an exchange, which routes them to queues via bindings and routing keys. We used the default exchange, which hides all of that behind a queue name. You will meet these words on the job โ the official tutorials below introduce them well. One more term for your radar: messages that keep failing are usually parked in a dead-letter queue so they stop blocking the rest.
</aside>
| Piece | What it does |
|---|---|
new Queue("tasks") bean |
Declares the queue on the broker at startup |
rabbitTemplate.convertAndSend(queue, message) |
Produces a message |
@RabbitListener(queues = ...) |
Consumes messages, one method call per message |
Ports 5672 / 15672 |
AMQP for applications / management UI for humans |
| Acknowledgement | Broker deletes the message only after success; otherwise it redelivers |
You ran a broker in Docker Compose, produced messages from a Spring endpoint, consumed them with @RabbitListener, and watched work wait in the queue while the application was down. Everything here โ queues, acknowledgements, at-least-once, idempotency โ transfers directly to managed brokers like Amazon SQS and Azure Service Bus.
The HackYourFuture curriculum is licensed underย CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with โค๏ธ by the HackYourFuture community ยท Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.