A topic exchange in RabbitMQ doesn’t just route messages; it filters them based on a pattern that’s more powerful than simple wildcards, allowing for complex routing logic that’s often misunderstood.

Let’s see it in action. Imagine you have a system that publishes events about different types of user activity. You want to route these events to different consumers based on the granularity of the activity.

First, we’ll create a topic exchange named user_activity_exchange.

rabbitmqadmin declare exchange name=user_activity_exchange type=topic

Now, let’s say we have three consumers:

  1. A consumer interested in all user activity.
  2. A consumer interested in all login-related activity.
  3. A consumer interested in all activity related to the admin user.

We’ll bind queues to this exchange with specific routing keys:

  • For the "all activity" consumer, we bind all_activity_queue to user_activity_exchange with the routing key #. The # (hash) is a special wildcard in RabbitMQ topic exchanges that matches zero or more words.

    rabbitmqadmin declare queue name=all_activity_queue
    rabbitmqadmin declare binding source=user_activity_exchange destination=all_activity_queue routing_key=#
    
  • For the "all login activity" consumer, we bind login_activity_queue to user_activity_exchange with the routing key user.login.*. The * (asterisk) wildcard matches exactly one word in the routing key. So, this will match user.login.success and user.login.failed, but not user.login.attempt.

    rabbitmqadmin declare queue name=login_activity_queue
    rabbitmqadmin declare binding source=user_activity_exchange destination=login_activity_queue routing_key=user.login.*
    
  • For the "admin user activity" consumer, we bind admin_user_activity_queue to user_activity_exchange with the routing key user.*.admin. This will match user.login.admin and user.logout.admin, but not user.admin.login or user.activity.admin.

    rabbitmqadmin declare queue name=admin_user_activity_queue
    rabbitmqadmin declare binding source=user_activity_exchange destination=admin_user_activity_queue routing_key=user.*.admin
    

Now, let’s publish some messages:

  1. Publish a message with routing key user.login.success:

    rabbitmqadmin publish exchange=user_activity_exchange routing_key=user.login.success payload="Admin logged in successfully"
    

    This message will be routed to all_activity_queue (because # matches user.login.success) and login_activity_queue (because user.login.* matches user.login.success). It will not go to admin_user_activity_queue because user.*.admin does not match user.login.success.

  2. Publish a message with routing key user.logout.admin:

    rabbitmqadmin publish exchange=user_activity_exchange routing_key=user.logout.admin payload="Admin logged out"
    

    This message will be routed to all_activity_queue (because # matches user.logout.admin) and admin_user_activity_queue (because user.*.admin matches user.logout.admin). It will not go to login_activity_queue because user.login.* does not match user.logout.admin.

  3. Publish a message with routing key user.profile.update:

    rabbitmqadmin publish exchange=user_activity_exchange routing_key=user.profile.update payload="User profile updated"
    

    This message will only be routed to all_activity_queue because only # matches user.profile.update.

The mental model here is that a topic exchange treats routing keys as dot-separated words. The * wildcard matches a single word, and the # wildcard matches zero or more words. When a message is published, RabbitMQ compares the message’s routing key against the binding keys for all queues attached to the topic exchange. A queue receives a message if its binding key matches the message’s routing key according to these wildcard rules.

Crucially, the routing key doesn’t have to be a direct match. The power of topic exchanges lies in their ability to match patterns. A single message published with a specific routing key can be delivered to multiple queues if their respective binding keys match that routing key. This is how you achieve flexible fan-out and filtering based on message content characteristics.

The most surprising thing is how the # wildcard operates: it can match nothing. If you bind a queue with a routing key of just #, it will receive every message published to that exchange, regardless of what the routing key is. This is a common way to create a "catch-all" or logging queue.

Understanding the precise behavior of * and # is key to designing robust message routing systems.

The next concept to explore is how to handle message durability and dead-lettering within this topic-based routing setup.

Want structured learning?

Take the full Rabbitmq course →