--- name: rabbitmq-setup description: "Add RabbitMQ producers and consumers to an existing Spring Boot 4 Maven project — Spring AMQP config, JSON messages, dead-letter exchanges, Testcontainers tests, compose wiring. Use when asked to add RabbitMQ, AMQP, or work queues. If messaging is requested without naming a broker, ask Kafka or RabbitMQ first." --- # RabbitMQ Setup Skill Adds AMQP messaging to an existing Spring Boot 4 project using Spring AMQP and Testcontainers. `SKILL_DIR` = directory containing this SKILL.md file. Load `SKILL_DIR/references/rabbitmq-conventions.md` before writing producers/consumers — it covers exchange/queue naming, routing keys, TTL/DLX patterns, and the Testcontainers RabbitMQ class. --- ## Step 0 — Gather inputs If the user asked for messaging without naming a broker, ask Kafka or RabbitMQ before going further. | Field | Required | Notes | |-------|----------|-------| | `exchanges` | Yes | list of exchanges, e.g. `job.events` | | `queues` | Yes | list of queues with bindings, e.g. `job.created.queue` bound to `job.events` with rk `job.created` | | `dlx` | No | `true` (default) — add dead-letter exchange and TTL retry queues | --- ## Step 1 — Read the project ```bash cat pom.xml find src/main/java -name '*Controller.java' | head -10 ls src/test/java ``` Confirm Spring Boot 4.x and a web dependency. --- ## Step 2 — Add dependencies Add to `pom.xml`: ```xml org.springframework.boot spring-boot-starter-amqp org.testcontainers testcontainers-rabbitmq test ``` Spring Boot 4.1's BOM pins Spring AMQP and Testcontainers 2.x. Never override `testcontainers.version` or `spring-amqp.version` by hand. --- ## Step 3 — RabbitMQ config Create `src/main/java//shared/config/RabbitmqConfig.java`: ```java package .shared.config; import org.springframework.amqp.core.*; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitmqConfig { public static final String JOB_EVENTS_EXCHANGE = "job.events"; public static final String JOB_CREATED_QUEUE = "job.created.queue"; public static final String JOB_CREATED_DLQ = "job.created.dlq"; public static final String JOB_EVENTS_DLX = "job.events.dlx"; @Bean public TopicExchange jobEventsExchange() { return ExchangeBuilder.topicExchange(JOB_EVENTS_EXCHANGE).durable(true).build(); } @Bean public Queue jobCreatedQueue() { return QueueBuilder.durable(JOB_CREATED_QUEUE) .withArgument("x-dead-letter-exchange", JOB_EVENTS_DLX) .withArgument("x-message-ttl", 30000) .build(); } @Bean public Binding jobCreatedBinding() { return BindingBuilder .bind(jobCreatedQueue()) .to(jobEventsExchange()) .with("job.created"); } @Bean public TopicExchange jobEventsDlx() { return ExchangeBuilder.topicExchange(JOB_EVENTS_DLX).durable(true).build(); } @Bean public Queue jobCreatedDlq() { return QueueBuilder.durable(JOB_CREATED_DLQ).build(); } @Bean public Binding jobCreatedDlqBinding() { return BindingBuilder .bind(jobCreatedDlq()) .to(jobEventsDlx()) .with("job.created"); } @Bean public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate template = new RabbitTemplate(connectionFactory); template.setMessageConverter(new Jackson2JsonMessageConverter()); return template; } } ``` Adjust exchange type (`direct`, `topic`, `fanout`) per domain need. Topic is the default for event-driven routing. --- ## Step 4 — Producer service Create `src/main/java//shared/rabbitmq/RabbitmqPublisher.java`: ```java package .shared.rabbitmq; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.stereotype.Service; @Service public class RabbitmqPublisher { private final RabbitTemplate rabbitTemplate; public RabbitmqPublisher(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } public void publish(String exchange, String routingKey, Object payload) { rabbitTemplate.convertAndSend(exchange, routingKey, payload); } } ``` Wrap in domain publishers: `JobEventPublisher.publishJobCreated(JobCreatedEvent event)`. --- ## Step 5 — Consumer template Create `src/main/java//job/JobEventConsumer.java`: ```java package .job; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; @Component public class JobEventConsumer { @RabbitListener(queues = RabbitmqConfig.JOB_CREATED_QUEUE) public void handleJobCreated(@Payload JobCreatedEvent event) { // idempotent handler } } ``` For retries with DLX: the queue declaration above already routes failed messages to the DLX after TTL. For explicit NACK to DLX, throw `AmqpRejectAndDontRequeueException`. --- ## Step 6 — Event records Create event records in the domain package: ```java package .job; import java.time.Instant; public record JobCreatedEvent(Long jobId, String title, Instant occurredAt) { } ``` Event records should be versioned for long-term storage. Include `schemaVersion` if events persist. --- ## Step 7 — Update application.yml Add: ```yaml spring: rabbitmq: host: ${RABBITMQ_HOST:localhost} port: ${RABBITMQ_PORT:5672} username: ${RABBITMQ_USER:guest} password: ${RABBITMQ_PASSWORD:guest} listener: simple: default-requeue-rejected: false concurrency: 1 max-concurrency: 5 ``` `default-requeue-rejected: false` ensures failed messages go to the DLX instead of being requeued indefinitely. --- ## Step 8 — Testcontainers base test If `BaseIntegrationTest` exists, add a RabbitMQ bean to the `IntegrationTestContainers` configuration it imports: ```java @Bean @ServiceConnection RabbitMQContainer rabbitmqContainer() { return new RabbitMQContainer(DockerImageName.parse("rabbitmq:4-management-alpine")); } ``` `RabbitMQContainer` is `org.testcontainers.rabbitmq.RabbitMQContainer`; Boot's `RabbitContainerConnectionDetailsFactory` sets host, port, and credentials. Tests extend `BaseIntegrationTest` as before — no separate base class, no `@DynamicPropertySource`, and no `static {}` start. If the project's base class still declares a `static @Container`, move that container into `IntegrationTestContainers` first (see `spring-testing` Step 3): the JUnit extension stops it after the first test class while Spring keeps the cached context. Before writing, confirm `4-management-alpine` is still current: ```bash curl -s "https://hub.docker.com/v2/repositories/library/rabbitmq/tags/4-management-alpine" \ | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['name'], d['last_updated'][:10])" ``` If `BaseIntegrationTest` does not exist, create one following `spring-scaffold` first. --- ## Step 9 — Write tests Create `src/test/java//job/JobEventConsumerIntegrationTest.java`: ```java package .job; import .BaseIntegrationTest; import .shared.config.RabbitmqConfig; import .shared.rabbitmq.RabbitmqPublisher; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import java.time.Duration; import java.time.Instant; import static org.awaitility.Awaitility.await; class JobEventConsumerIntegrationTest extends BaseIntegrationTest { @Autowired private RabbitmqPublisher publisher; @Test void should_consume_job_created_event() { publisher.publish(RabbitmqConfig.JOB_EVENTS_EXCHANGE, "job.created", new JobCreatedEvent(1L, "Backend Engineer", Instant.now())); await().atMost(Duration.ofSeconds(10)) .untilAsserted(() -> { // assert side effect or spy on handler }); } } ``` --- ## Step 10 — docker-compose service (optional) If `docker-compose.yml` exists, add a RabbitMQ service: ```yaml rabbitmq: image: rabbitmq:4-management-alpine ports: - "5672:5672" - "15672:15672" environment: RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-guest} RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-guest} volumes: - rabbitmq_data:/var/lib/rabbitmq healthcheck: test: ["CMD", "rabbitmq-diagnostics", "ping"] interval: 10s timeout: 5s retries: 5 ``` Add `rabbitmq_data:` to the top-level `volumes` block. --- ## Step 11 — Run and report ```bash ./mvnw test ``` Report: - Dependencies added - `RabbitmqConfig`, `RabbitmqPublisher`, example consumer/event created - Exchanges, queues, bindings, and DLX configured - Test result summary - Next step: add `api-design` to document event schemas or `spring-security` to secure AMQP flows