Skip to content

Topic Exchange

A topic exchange matches a message’s routing key against a pattern in each binding. It’s the most flexible of the exchange types: direct routing is “equals”, fanout is “everything”, and topic is “matches this shape”. It lets subscribers say “I want these kinds of events” without the producer knowing who’s listening.

The routing key is structured as words separated by dots: order.created, payment.failed.th, sensor.temperature.warehouse-3. Binding keys use the same dotted form plus two wildcards:

  • * (star) matches exactly one word.
  • # (hash) matches zero or more words.
flowchart LR
  p["publish
'order.eu.created'"] --> x["topic exchange
'events'"]
  x -->|"order.*.created"| q1["queue: new-orders"]
  x -->|"order.#"| q2["queue: all-order-events"]
  x -. "no match: payment.#" .-> q3["queue: payments"]
A topic exchange matching a dotted key against binding patterns

Walk through a key of order.eu.created:

  • order.*.createdmatches (* absorbs eu, the first and last words are literal).
  • order.#matches (# absorbs eu.created, any number of trailing words).
  • payment.#no match (first word must be payment).
  • order.*no match (* is exactly one word, but there are two words after order).

That last one is the classic gotcha: * is exactly one word, not “one or more”. Use # when the tail length varies.

const ex = 'events';
await channel.assertExchange(ex, 'topic', { durable: true });
// "all created orders, in any region"
const q = await channel.assertQueue('new-orders', { durable: true });
await channel.bindQueue(q.queue, ex, 'order.*.created');
// producer: the routing key describes the event
channel.publish(ex, 'order.eu.created', Buffer.from('...'));

A topic exchange can imitate the other two:

  • A binding key with no wildcards (order.created) behaves exactly like a direct binding.
  • A binding key of just # matches everything, behaving like a fanout.

Because of this, many teams default to a topic exchange for event buses — it costs nothing extra and leaves room to add finer-grained subscribers later. The trade-off is discipline: a good, consistent routing-key scheme (domain.detail.action) is what makes topic routing readable instead of a tangle.

In a topic exchange, what does the * wildcard match?
Which binding pattern matches the routing key "payment.failed.th"?
How can a topic exchange behave like a fanout exchange?
Why do many teams default to a topic exchange for an event bus?