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:
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:
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:
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
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: trueat publish time. - Getting
*and#backwards in a topic binding pattern —*is exactly one word,#is zero or more;order.*will not matchorder.eu.created(two words afterorder), butorder.#will.