Exchanges, Queues & Bindings

Direct, fanout, and topic exchanges with concrete routing-key examples, plus durable vs transient queues.

Exchanges route, queues hold

A producer never publishes directly to a queue — it publishes to an exchange, and a binding tells the exchange which queue(s) a given message should end up in. The exchange type determines how it uses the message's routing key to make that decision.

Direct exchange: exact routing key match

A message is routed to every queue bound with a routing key that exactly matches the message's routing key:

Text
Exchange: orders.direct  (type = direct)

Queue: orders.created.queue    --bound with routing key "order.created"
Queue: orders.cancelled.queue  --bound with routing key "order.cancelled"

Publish routing key "order.created"   --> orders.created.queue only
Publish routing key "order.cancelled" --> orders.cancelled.queue only

Use a direct exchange when you have a small, fixed set of distinct message types and each should go to exactly one place.

Fanout exchange: broadcast to everyone

A fanout exchange ignores the routing key entirely and delivers every message to every bound queue:

Text
Exchange: orders.fanout  (type = fanout)

Queue: notifications.queue  --bound (routing key ignored)
Queue: analytics.queue      --bound (routing key ignored)
Queue: audit.queue          --bound (routing key ignored)

Publish anything --> delivered to all three queues

This is the natural fit for "broadcast an event to however many independent, unrelated consumers care about it" — an OrderPlaced event reaching a notifications service, an analytics pipeline, and an audit log simultaneously, none of them aware the others exist.

Topic exchange: pattern-based routing

A topic exchange routes using wildcard pattern matching against a dot-separated routing key. * matches exactly one word; # matches zero or more words:

Text
Exchange: orders.topic  (type = topic)

Queue: eu-orders.queue    --bound with pattern "order.eu.*"
Queue: all-created.queue  --bound with pattern "order.*.created"
Queue: everything.queue   --bound with pattern "order.#"

Publish routing key "order.eu.created":
  matches "order.eu.*"    --> eu-orders.queue     YES
  matches "order.*.created" --> all-created.queue YES
  matches "order.#"       --> everything.queue     YES

Publish routing key "order.us.cancelled":
  matches "order.eu.*"    --> eu-orders.queue     NO
  matches "order.*.created" --> all-created.queue NO
  matches "order.#"       --> everything.queue     YES

Topic exchanges are the right choice whenever different consumers care about different slices of the same event stream, sliced along more than one dimension (region, event type, or both at once, as above) — something a direct exchange's exact-match routing can't express.

Declaring exchanges, queues, and bindings in code

Javascript
const amqp = require('amqplib');

async function setupTopology() {
  const connection = await amqp.connect('amqp://localhost');
  const channel = await connection.createChannel();

  await channel.assertExchange('orders.topic', 'topic', { durable: true });
  await channel.assertQueue('eu-orders.queue', { durable: true });
  await channel.bindQueue('eu-orders.queue', 'orders.topic', 'order.eu.*');
}

assertExchange/assertQueue are idempotent — they create the resource if it doesn't exist and simply verify its properties match if it does, so it's normal to call them at startup every time a service connects.

Durable vs. transient — and why both halves matter

Two independent settings control whether something survives a broker restart, and missing either one silently defeats the guarantee:

Setting Meaning
Queue durability { durable: true } on assertQueue The queue's definition survives a broker restart. A durable: false queue is gone (along with anything still in it) the moment the broker restarts.
Message persistence { persistent: true } on publish The individual message is written to disk, not just held in memory.

A durable queue with non-persistent messages loses every message still in the queue on restart, even though the queue itself comes back. A transient queue with persistent messages doesn't help either — the queue is gone, so the messages have nowhere to be. Both must be set for a message to reliably survive a broker restart.

Common mistakes

  • Choosing a fanout exchange for a case that actually needs selective routing (some consumers should only get a subset of messages) — leading to every consumer filtering messages it should never have received in the first place.
  • Publishing directly to a queue name via the default (nameless) exchange as a shortcut, then being confused later about why a topic exchange's bindings "aren't working" — that queue was never actually going through the topic exchange at all.
  • Assuming a durable queue alone protects messages across a restart, forgetting that the messages themselves also need persistent: true at publish time.
  • Getting * and # backwards in a topic binding pattern — * is exactly one word, # is zero or more; order.* will not match order.eu.created (two words after order), but order.# will.