Designing routing with topic exchanges

RabbitMQ · Practical · 10 min

A good routing scheme lets new consumers subscribe to exactly the events they need without touching producers.

Topic exchanges are the most flexible routing tool, and the quality of the design is in the routing keys.

Keys are dot-separated words, most general first: <domain>.<entity>.<event> or <region>.<service>.<severity>. Examples: orders.order.created, orders.order.cancelled, payments.refund.failed. Then each consumer binds with a pattern:

  • orders.order.*: every event about orders.
  • *.*.failed: failures across all domains.
  • orders.#: everything in the orders domain, at any depth.
  • #: everything, which is what an audit log or a debug tap uses.

Remember that * matches exactly one word, so orders.* does not match orders.order.created; # matches zero or more words. A binding without wildcards behaves like a direct binding.

Useful extras:

  • Exchange-to-exchange bindings: exchange_bind routes from one exchange to another, for example a per-team exchange fed by a central one.
  • Alternate exchange (alternate-exchange argument or policy): catches messages that match no binding, so they're stored for inspection instead of dropped.
  • Keep routing keys free of IDs. They're for routing, not data; IDs belong in the body or headers.

Document the key scheme like an API, since every consumer depends on it.

Example

import pika
ch = pika.BlockingConnection(pika.ConnectionParameters("localhost")).channel()
ch.confirm_delivery()
ch.exchange_declare("unrouted", "fanout")
ch.queue_declare("unrouted")
ch.queue_bind("unrouted", "unrouted")
ch.exchange_declare("events", "topic", arguments={"alternate-exchange": "unrouted"})

bindings = {"billing": "orders.order.*", "alerts": "*.*.failed", "audit": "#"}
for queue, pattern in bindings.items():
    ch.queue_declare(queue)
    ch.queue_bind(queue, "events", routing_key=pattern)

for key in ["orders.order.created", "payments.refund.failed", "orders.cart.updated"]:
    ch.basic_publish("events", key, body=key.encode())

for q in ["billing", "alerts", "audit", "unrouted"]:
    print(q, ch.queue_declare(q, passive=True).method.message_count)
# billing 1   alerts 1   audit 3   unrouted 0  (audit's # matched everything)

Remember

  • Routing keys: dot-separated words, general to specific
  • * is exactly one word, # is zero or more
  • Alternate exchanges catch unroutable messages

Watch out for

  • Binding orders.* and expecting it to match three-word keys like orders.order.created.

Check yourself

  1. Which binding pattern matches payments.refund.failed?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.rabbitmq.com/tutorials/tutorial-five-python