Messaging Patterns — Cheat Sheet
RabbitMQ · 4 topics. Download the PDF or the Instagram carousel and share it.
Cheat Sheet · AiCanCode.org
Messaging Patterns
RabbitMQ4 topicsQuick revision reference
1
Work Queues (Task Distribution)
Multiple workers consume from a single queue; with prefetch=1 and manual ack, work is evenly distributed to idle workers rather than buffered on a busy one.
- ✓Work queues distribute tasks across multiple competing workers — each task is delivered to exactly one worker.
- ✓prefetch=1 (basicQos) + manual ack = fair dispatch: a busy worker does not receive new tasks until it acks the current one.
- ✓Without prefetch=1, RabbitMQ round-robins messages regardless of worker speed, causing uneven load distribution.
- ✓Durable queue + persistent messages (deliveryMode=2) ensures no task loss on broker restart.
- ✓Scale workers horizontally by increasing Kubernetes replicas or @RabbitListener concurrency; queue depth is the scaling signal.
- ✓KEDA can autoscale K8s Deployments based on RabbitMQ queue depth — scale to zero when queue is empty.
Java — Work Queue Producer + Worker
// Producer — create work and publish
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection conn = factory.newConnection();
Channel channel = conn.createChannel()) {
// Durable queue — survives restart
channel.queueDeclare("task_queue", true, false, false, null);
String[] tasks = {"resize_image_1", "send_email_2", "process_payment_3"};
for (String task : tasks) {
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // persistent — survives restart
.build();
channel.basicPublish("", "task_queue", props, task.getBytes());
System.out.println("Sent: " + task);
}
}
// Worker — multiple instances of this can run in parallel
try (Connection conn = factory.newConnection();
Channel channel = conn.createChannel()) {
channel.queueDeclare("task_queue", true, false, false, null);
channel.basicQos(1); // fair dispatch — max 1 unacked message at a time
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String task = new String(delivery.getBody());
System.out.println("Processing: " + task);
try {
doWork(task); // potentially slow
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // ack on success
} catch (Exception e) {
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true); // requeue on failure
}
};
channel.basicConsume("task_queue", false, deliverCallback, tag -> {});
}2
Publish-Subscribe Pattern
Fanout exchange broadcasts events to all bound queues; each consumer group has its own queue, receiving every event independently for fan-out notification scenarios.
- ✓Fanout exchange ignores routing keys and delivers to all bound queues — ideal for broadcasting domain events
- ✓Each subscriber gets its own queue; multiple instances of the same subscriber share one queue for load balancing
- ✓Topic exchanges extend pub/sub with wildcard routing keys: * (one word) and # (zero or more words)
- ✓Producers are decoupled from consumers — new subscribers can be added without any publisher changes
- ✓Use durable queues + persistent messages for subscribers that must not miss events during restart
- ✓Auto-delete queues (autoDelete=true) are useful for transient subscribers like live dashboard WebSocket feeds
Java — Spring AMQP fanout exchange with multiple subscriber queues
// Spring AMQP fanout pub/sub configuration
@Configuration
public class PubSubConfig {
@Bean
public FanoutExchange orderEventsExchange() {
return new FanoutExchange("order.events", true, false);
}
@Bean public Queue emailNotificationQueue() {
return QueueBuilder.durable("order.events.email").build();
}
@Bean public Queue inventoryUpdateQueue() {
return QueueBuilder.durable("order.events.inventory").build();
}
@Bean public Queue analyticsQueue() {
return QueueBuilder.durable("order.events.analytics").build();
}
// Bind all queues to the exchange — routing key ignored for fanout
@Bean public Binding emailBinding(FanoutExchange ex, Queue emailNotificationQueue) {
return BindingBuilder.bind(emailNotificationQueue).to(ex);
}
@Bean public Binding inventoryBinding(FanoutExchange ex, Queue inventoryUpdateQueue) {
return BindingBuilder.bind(inventoryUpdateQueue).to(ex);
}
@Bean public Binding analyticsBinding(FanoutExchange ex, Queue analyticsQueue) {
return BindingBuilder.bind(analyticsQueue).to(ex);
}
}3
Request-Reply (RPC) Pattern
Producer sends a message with a reply-to queue and correlation-id; consumer processes the request and publishes the response to the reply-to queue; producer correlates the reply.
- ✓RPC over AMQP uses replyTo (temporary queue) and correlationId to match requests to responses
- ✓Spring AMQP's convertSendAndReceive() handles reply-to queue creation and correlation automatically
- ✓Direct Reply-To (amq.rabbitmq.reply-to) avoids creating a new queue per client
- ✓Always set a replyTimeout — a missing or crashed server will never send a reply
- ✓Synchronous RPC over messaging couples services; prefer async callbacks for resilience
- ✓The server must publish the response with the same correlationId from the request
Java — manual RPC over AMQP
// Client side
String replyQueue = channel.queueDeclare("", false, true, true, null).getQueue();
Map<String, CompletableFuture<byte[]>> pending = new ConcurrentHashMap<>();
channel.basicConsume(replyQueue, true, (tag, msg) -> {
String corrId = msg.getProperties().getCorrelationId();
CompletableFuture<byte[]> future = pending.remove(corrId);
if (future != null) future.complete(msg.getBody());
}, tag -> {});
// Send request
String corrId = UUID.randomUUID().toString();
CompletableFuture<byte[]> result = new CompletableFuture<>();
pending.put(corrId, result);
AMQP.BasicProperties props = new AMQP.BasicProperties.Builder()
.correlationId(corrId)
.replyTo(replyQueue)
.build();
channel.basicPublish("", "rpc_queue", props, "42".getBytes());
byte[] response = result.get(5, TimeUnit.SECONDS);
// Server side
channel.basicConsume("rpc_queue", false, (tag, msg) -> {
byte[] body = processRequest(msg.getBody());
AMQP.BasicProperties replyProps = new AMQP.BasicProperties.Builder()
.correlationId(msg.getProperties().getCorrelationId())
.build();
channel.basicPublish("", msg.getProperties().getReplyTo(), replyProps, body);
channel.basicAck(msg.getEnvelope().getDeliveryTag(), false);
}, tag -> {});4
Competing Consumers Pattern
Multiple consumer instances all subscribe to the same queue; the broker delivers each message to exactly one consumer, providing natural horizontal scaling for processing.
- ✓Competing consumers deliver each message to exactly one worker — natural horizontal scaling
- ✓prefetch=1 + manual ack ensures fair dispatch; workers only receive work they can handle
- ✓RabbitMQ round-robins messages across consumers; slow workers won't block fast ones with prefetch=1
- ✓Crashed workers: unacked messages are re-queued and delivered to another worker
- ✓Competing consumers break message ordering — use consistent hashing if order matters
- ✓Design workers to be idempotent — re-delivery after crash must be safe to re-process
Spring AMQP — competing consumers with manual ack
// Worker (same code, deployed N times)
@Component
class OrderWorker {
@RabbitListener(
queues = "order-processing",
containerFactory = "workerContainerFactory"
)
public void process(Order order, Channel channel,
@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
try {
orderService.fulfil(order);
channel.basicAck(tag, false); // done
} catch (RecoverableException e) {
channel.basicNack(tag, false, true); // requeue for retry
} catch (Exception e) {
channel.basicNack(tag, false, false); // → DLX, no requeue
}
}
}
@Bean
SimpleRabbitListenerContainerFactory workerContainerFactory(ConnectionFactory cf) {
var f = new SimpleRabbitListenerContainerFactory();
f.setConnectionFactory(cf);
f.setPrefetchCount(1); // fair dispatch
f.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return f;
}Learn this free with Aria, your AI tutor → AiCanCode.org/learn/rabbitmq