Topic Exchange
Routing by pattern
Section titled “Routing by pattern”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.
Reading the wildcards
Section titled “Reading the wildcards”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"]
Walk through a key of order.eu.created:
order.*.created→ matches (*absorbseu, the first and last words are literal).order.#→ matches (#absorbseu.created, any number of trailing words).payment.#→ no match (first word must bepayment).order.*→ no match (*is exactly one word, but there are two words afterorder).
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 eventchannel.publish(ex, 'order.eu.created', Buffer.from('...'));ex = "events"channel.exchange_declare(exchange=ex, exchange_type="topic", durable=True)
# "all created orders, in any region"channel.queue_declare(queue="new-orders", durable=True)channel.queue_bind(queue="new-orders", exchange=ex, routing_key="order.*.created")
# producer: the routing key describes the eventchannel.basic_publish(exchange=ex, routing_key="order.eu.created", body="...")ex := "events"ch.ExchangeDeclare(ex, "topic", true, false, false, false, nil)
// "all created orders, in any region"ch.QueueDeclare("new-orders", true, false, false, false, nil)ch.QueueBind("new-orders", "order.*.created", ex, false, nil)
// producer: the routing key describes the eventch.PublishWithContext(ctx, ex, "order.eu.created", false, false, amqp.Publishing{Body: []byte("...")})Topic subsumes direct and fanout
Section titled “Topic subsumes direct and fanout”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.