Week 14

Architecture Patterns

Communication Patterns

Background Processing

Architectural Trade-offs

Messaging

Hands-on: RabbitMQ

Practice

Assignment

Backend Track

Introduction

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.

Step 1 โ€” Run RabbitMQ with Docker Compose

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>

Step 2 โ€” Add Spring AMQP

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

Step 3 โ€” Declare a queue and produce messages

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>

Step 4 โ€” Consume with a worker

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>

Step 5 โ€” The demo that explains everything

  1. Stop the Spring application completely.
  2. In the management UI, open Queues โ†’ 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.
  3. Start the application and watch the logs drain the queue.

<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>

Acknowledgement and at-least-once delivery

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.

What we deliberately skipped

<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>

Quick reference

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

Summary

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.

Extra resources


The HackYourFuture curriculum is licensed underย CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with โค๏ธ by the HackYourFuture community ยท Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.