A RabbitMQ topic exchange doesn’t actually filter messages; it routes them based on a pattern, and your consumers decide what to do with what they receive.

Let’s see this in action. Imagine we have a system that publishes various types of events: user.created, user.deleted, order.placed, order.shipped.

We’ll set up a topic exchange named event_exchange.

# RabbitMQ Management UI (or rabbitmqadmin)
# Declare an exchange:
rabbitmqadmin declare exchange name=event_exchange type=topic durable=true

# Declare queues:
rabbitmqadmin declare queue name=user_events durable=true
rabbitmqadmin declare queue name=order_events durable=true
rabbitmqadmin declare queue name=all_events durable=true

# Bind queues to the exchange:
# Bind user_events to anything starting with 'user.'
rabbitmqadmin declare binding source=event_exchange destination=user_events routing_key=user.#

# Bind order_events to anything starting with 'order.' followed by 'placed' or 'shipped'
rabbitmqadmin declare binding source=event_exchange destination=order_events routing_key=order.placed
rabbitmqadmin declare binding source=event_exchange destination=order_events routing_key=order.shipped

# Bind all_events to everything
rabbitmqadmin declare binding source=event_exchange destination=all_events routing_key=#

Now, when a message is published to event_exchange with a routing key like user.created:

  • The user.created routing key matches user.# on the user_events queue.
  • The user.created routing key also matches # on the all_events queue.
  • It does not match order.placed or order.shipped on the order_events queue.

So, the message lands in user_events and all_events.

If a message is published with order.placed:

  • It matches order.placed on the order_events queue.
  • It matches # on the all_events queue.
  • It does not match user.#.

The message lands in order_events and all_events.

This routing is based on pattern matching of the routing key against the binding keys. The . acts as a separator, # matches zero or more words, and * matches exactly one word.

The problem this solves is flexible message distribution. Instead of having a queue for every single message type, you can group them. A single consumer bound to user.# can get all user-related events without needing separate bindings for user.created, user.deleted, user.updated, etc.

The core of the topic exchange’s power lies in the flexibility of its binding keys. You can define very granular subscriptions. For instance, to get all orders that have been placed but not shipped, you might bind a queue with order.placed and another with order.shipped, and then filter in your application logic. Or, you could try to get clever with bindings. A binding key like order.*.processing would match order.new.processing and order.old.processing but not order.processing.

The actual filtering, the decision of whether to process a message once it arrives, still happens in your consumer application. The exchange’s job is purely to deliver messages to queues whose bindings match the routing key. If a message arrives with a routing key that doesn’t match any bindings, it’s silently dropped by default (unless you have a dead-lettering exchange configured).

If you want to send a message with a routing key that contains a period, like system.status.ok, and you have a binding that uses system.*.ok, the * will match status and the message will be routed. If you had a binding system.#.ok, it would also match system.status.ok.

The next hurdle is managing message ordering when using topic exchanges across multiple queues and consumers.

Want structured learning?

Take the full Amqp course →