RabbitMQ doesn’t queue events; it routes messages that represent events to queues, and consumers then process those messages.

Let’s see this in action. Imagine a simple e-commerce system. When a new order is placed, an order.created event is published. We want this event to trigger a few things:

  1. Send a confirmation email: A service responsible for sending emails needs to know an order was created.
  2. Update inventory: An inventory management service needs to decrement stock.
  3. Notify shipping: A shipping service needs to prepare for fulfillment.

Here’s a basic RabbitMQ setup for this:

Publisher (e.g., Order Service):

import pika
import json

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# Declare an exchange (fanout type broadcasts to all queues bound to it)
channel.exchange_declare(exchange='events', exchange_type='fanout')

order_data = {
    "order_id": "ORD12345",
    "customer_id": "CUST987",
    "items": [{"product_id": "PROD001", "quantity": 2}],
    "timestamp": "2023-10-27T10:00:00Z"
}

# Publish the message to the 'events' exchange
channel.basic_publish(
    exchange='events',
    routing_key='', # For fanout, routing_key is ignored
    body=json.dumps(order_data)
)
print(" [x] Sent order.created event")
connection.close()

Consumers (e.g., Email Service, Inventory Service, Shipping Service):

First, each consumer needs to declare the events exchange and then bind a unique queue to it.

# --- Email Service Consumer ---
import pika
import json

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='events', exchange_type='fanout')

# Declare a unique queue for the email service
result = channel.queue_declare(queue='', exclusive=True)
queue_name = result.method.queue

# Bind the queue to the 'events' exchange. For fanout, the binding_key is ignored.
channel.queue_bind(exchange='events', queue=queue_name)

def callback(ch, method, properties, body):
    order_event = json.loads(body)
    print(f" [x] Received by Email Service: {order_event['order_id']}")
    # Logic to send confirmation email...

channel.basic_consume(queue=queue_name, on_message_callback=callback, auto_ack=True)

print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

You’d run similar consumer code for the Inventory Service and Shipping Service, each declaring their own unique queue and binding it to the events exchange.

The magic here is that when the order.created message is published to the events exchange, RabbitMQ, because it’s a fanout exchange, will deliver a copy of that message to every single queue that is bound to it. Each consumer is listening to its own queue, so they all receive the same event independently.

The Mental Model:

Think of RabbitMQ as a postal service for your applications.

  • Exchanges: These are like post offices. They receive mail (messages) and decide where to send it based on rules.
    • fanout: A post office that makes a photocopy of every letter it receives and sends a copy to every single mailbox (queue) connected to it. No sorting, just broadcasting.
    • direct: A post office that looks at the "street address" (routing key) on the letter and delivers it only to mailboxes that have that exact address registered.
    • topic: A post office that uses more flexible "street addresses" (routing keys) with wildcards (like order.* or *.created). It delivers letters to mailboxes that match the pattern.
    • headers: A post office that looks at "stamps" or "labels" (headers) on the letter, not the address, to decide where to send it.
  • Queues: These are the mailboxes. They hold the letters until someone (a consumer) picks them up. A queue can have multiple consumers attached, but each message in the queue is only delivered to one of those consumers (round-robin).
  • Bindings: These are the agreements between a post office (exchange) and a mailbox (queue). They specify which types of mail the mailbox should receive from the post office. For fanout, it’s "all mail." For direct and topic, it’s based on the routing key.
  • Messages: These are the actual letters. They contain the data (the event payload) and metadata (routing key, headers).
  • Publishers: The people sending letters. They send them to a specific post office (exchange).
  • Consumers: The people picking up mail from their mailboxes (queues).

This setup decouples the publisher from the consumers. The order service doesn’t need to know who needs to know about an order being created, or even how many services need to know. It just publishes the event to the events exchange, and RabbitMQ handles the distribution.

The most counterintuitive part is how RabbitMQ handles message delivery to multiple consumers attached to the same queue. While a fanout exchange ensures each queue gets a copy of the message, if you have multiple consumers subscribed to that single queue, RabbitMQ will deliver each message to only one of those consumers. It distributes the work. If you want every consumer to process every message, they must each have their own queue bound to the exchange.

The next step is often understanding how to make these event streams durable and ensure messages aren’t lost if RabbitMQ restarts, which leads into message acknowledgments and durable queues.

Want structured learning?

Take the full Event-driven course →