Read time: ~

Exchanges, Bindings and Routing Topologies

Direct, topic, fanout, and headers exchanges, the default and alternate exchange, and exchange-to-exchange bindings.

Prerequisite:AMQP and the RabbitMQ ModelYou’ll need: A broker running locally from Environment Setup.

Routing is the part of RabbitMQ with no direct REST equivalent, and the part most new users get wrong. This module is the single source of truth for how exchanges decide where a message goes.


What you’ll be able to do after this module

  • Explain what a binding is and how a routing key is matched.
  • Choose the right exchange type for a given routing need.
  • Use topic wildcards (* and #) correctly.
  • Understand the default exchange and the alternate exchange for unroutable messages.
  • Declare each exchange type and its bindings with Spring AMQP.

1. Exchanges, bindings, and routing keys

A producer never publishes to a queue. It publishes to an exchange with a routing key. A binding is a rule that links an exchange to a queue. When a message arrives, the exchange compares the routing key (and its type’s rules) against its bindings and delivers a copy to every matching queue.

flowchart LR
    Producer["Order Service"]
    EX["Exchange"]
    Q1["Queue A"]
    Q2["Queue B"]
    Producer -->|"publish (routing key)"| EX
    EX -->|"binding match"| Q1
    EX -->|"binding match"| Q2

If no binding matches, the message is unroutable. By default it is silently dropped, which surprises people. Section 5 of this module covers how to catch those.

The exchange type determines how routing keys are matched. There are four types.


2. Direct exchange

A direct exchange delivers a message to the queues whose binding key exactly equals the message’s routing key.

flowchart LR
    P["Order Service"]
    DX["orders.direct<br/>(direct)"]
    QI["inventory.queue"]
    QN["notification.queue"]
    QC["cancellation.queue"]
    P -->|"order.created"| DX
    DX -->|"order.created"| QI
    DX -->|"order.created"| QN
    DX -->|"order.cancelled"| QC

Use it for exact, point-to-point routing. Multiple queues can bind with the same key to receive the same message.

@Bean
DirectExchange ordersDirect() {
    return new DirectExchange("orders.direct");
}

@Bean
Binding inventoryBinding(Queue inventoryQueue, DirectExchange ordersDirect) {
    return BindingBuilder.bind(inventoryQueue).to(ordersDirect).with("order.created");
}

3. Topic exchange

A topic exchange matches routing keys against binding patterns using two wildcards on dot-separated words:

WildcardMeaning
*matches exactly one word
#matches zero or more words

With routing keys like order.<region>.<event>, different consumers subscribe to different slices:

Binding patternMatchesDoes not match
order.eu.*order.eu.created, order.eu.cancelledorder.us.created, order.eu.created.retry
order.*.createdorder.eu.created, order.us.createdorder.eu.cancelled
order.#order.created, order.eu.created, order.eu.created.retrypayment.created
flowchart LR
    P["Order Service"]
    TX["orders.topic<br/>(topic)"]
    P -->|"order.eu.created"| TX
    TX -->|"order.eu.*"| QEU["eu.orders.queue"]
    TX -->|"order.*.created"| QNEW["new.orders.queue"]
    TX -->|"order.#"| QALL["audit.queue"]

Topic exchanges are the most flexible and the most common choice for event-driven systems.

@Bean
TopicExchange ordersTopic() {
    return new TopicExchange("orders.topic");
}

@Bean
Binding euBinding(Queue euOrdersQueue, TopicExchange ordersTopic) {
    return BindingBuilder.bind(euOrdersQueue).to(ordersTopic).with("order.eu.*");
}

4. Fanout exchange

A fanout exchange ignores the routing key entirely and delivers a copy to every bound queue. This is broadcast or publish/subscribe.

flowchart LR
    P["Order Service"]
    FX["orders.fanout<br/>(fanout)"]
    P -->|"(routing key ignored)"| FX
    FX --> QI["inventory.queue"]
    FX --> QN["notification.queue"]
    FX --> QA["analytics.queue"]

Use it when one event has many independent consumers and you do not need selective routing. Adding a new consumer is just a new queue bound to the exchange.

@Bean
FanoutExchange ordersFanout() {
    return new FanoutExchange("orders.fanout");
}

@Bean
Binding analyticsBinding(Queue analyticsQueue, FanoutExchange ordersFanout) {
    return BindingBuilder.bind(analyticsQueue).to(ordersFanout);
}

5. Headers exchange

A headers exchange routes on message header values instead of the routing key. Bindings specify header key/value pairs and an x-match rule of all (every header must match) or any (at least one).

flowchart LR
    P["Producer"]
    HX["orders.headers<br/>(headers)"]
    P -->|"headers: region=eu, priority=high"| HX
    HX -->|"x-match=all, region=eu, priority=high"| QVIP["eu.priority.queue"]

Headers exchanges are uncommon. Reach for a topic exchange first; use headers only when routing genuinely depends on several non-hierarchical attributes.


6. The default exchange and unroutable messages

Default exchange

Every broker has a nameless default exchange (a direct exchange with an empty name). Every queue is automatically bound to it using the queue’s own name as the routing key. That is why the rabbitmqadmin publish routing_key=demo.queue command in Environment Setup reached the queue directly. It is handy for quick tests, but real applications should declare their own exchanges so routing is explicit.

Alternate exchange

To avoid silently losing unroutable messages, declare an alternate exchange on your primary exchange. Any message the primary cannot route is sent to the alternate instead, where a catch-all queue can capture it for inspection.

flowchart LR
    P["Producer"]
    MX["orders.topic"]
    AX["orders.unrouted<br/>(alternate)"]
    P -->|"order.unknown.type"| MX
    MX -.->|"no binding matched"| AX
    AX --> QU["unrouted.queue"]
@Bean
TopicExchange ordersTopic() {
    return ExchangeBuilder.topicExchange("orders.topic")
            .withArgument("alternate-exchange", "orders.unrouted")
            .build();
}

Note: An alternate exchange catches messages the broker could not route. It is different from a dead-letter exchange, which catches messages a consumer rejected or that expired. Dead-lettering is covered in the Reliability section.


7. Exchange-to-exchange bindings

An exchange can be bound to another exchange, not just to queues. The message flows through the first exchange to the second, which applies its own routing. This lets you build layered topologies, for example a fanout that broadcasts into several topic exchanges.

@Bean
Binding exchangeToExchange(TopicExchange ordersTopic, FanoutExchange ordersFanout) {
    return BindingBuilder.bind(ordersTopic).to(ordersFanout);
}

Use this sparingly. It is powerful for large systems but adds indirection that can make routing harder to reason about.


8. Choosing an exchange type

NeedExchange type
Exact routing-key match to specific queuesDirect
Selective subscription by pattern or hierarchyTopic
Broadcast to all subscribersFanout
Routing on multiple header attributesHeaders

When in doubt, start with a topic exchange. It behaves like a direct exchange when you use exact keys, and gives you room to grow into patterns.


Checkpoint

You should now be able to:

  • Explain how a binding and routing key decide delivery.
  • Pick direct, topic, fanout, or headers for a given routing need.
  • Write topic patterns with * and # correctly.
  • Describe the default exchange and use an alternate exchange for unroutable messages.