skip to content

Compare DirectExchange, TopicExchange, and FanoutExchange in Spring AMQP. How does routing differ, and how do you build the binding for each?

level: middleimportance: must knowfreq 75%

answer

  1. Direct = exact key
  2. Topic = * one word, # zero-plus words
  3. Fanout = ignore key, broadcast
  4. Headers = match on headers, x-match all/any
  5. Fanout binding has no .with()

basics

~20 s

Direct matches the routing key exactly. Topic matches wildcard patterns (* = one word, # = zero+ words). Fanout ignores the routing key and copies to every bound queue. You bind Direct/Topic with .with(key); Fanout binds with no key.

solid answer

~40 s

The three common exchange types differ only in how they match a message's routing key against bindings. DirectExchange delivers to queues whose binding key equals the routing key exactly — good for point-to-point by name. TopicExchange treats keys as dot-delimited words and matches wildcard patterns: '*' matches exactly one word, '#' matches zero or more words (e.g. 'orders.*' or 'orders.#') — ideal for hierarchical event topics. FanoutExchange ignores the routing key entirely and broadcasts a copy to every bound queue — pub/sub. In Spring AMQP you build bindings with BindingBuilder: for Direct/Topic, bind(queue).to(exchange).with(pattern); for Fanout, bind(queue).to(fanoutExchange) with no key. There's also HeadersExchange, which routes on message header attributes instead of the routing key.

code

java · 16 lines
java
// Direct: exact routing-key match
Binding direct = BindingBuilder.bind(q1)
        .to(new DirectExchange("d.x")).with("orders.created");

// Topic: wildcard patterns (* = one word, # = zero+ words)
Binding topic = BindingBuilder.bind(q2)
        .to(new TopicExchange("t.x")).with("order.*.eu");

// Fanout: no routing key, broadcast to every bound queue
Binding fanout = BindingBuilder.bind(q3)
        .to(new FanoutExchange("f.x"));

// Headers: route on header attributes, not the routing key
Binding headers = BindingBuilder.bind(q4)
        .to(new HeadersExchange("h.x"))
        .where("format").matches("pdf");

go deeper

for a junior

Should recognize the three types by name and that fanout broadcasts.

for a middle

Must correctly state * vs # semantics and build bindings for each type.

for a senior

Should reason about type-mismatch redeclaration failures and when Topic beats Fanout/Direct.

for a principal

Discusses topology design trade-offs, header exchanges, and evolution/immutability of exchange type.

## What an exchange *type* controls Every exchange receives (message, routingKey) pairs. The **type** is the algorithm it uses to decide which bound queues match. Spring AMQP models each type as a distinct class in `org.springframework.amqp.core`. ### DirectExchange Delivers a message to every queue whose **binding key is exactly equal** to the message's routing key. Use it for straightforward routing-by-name (e.g. `orders.created` -> the orders queue). Multiple queues can share the same binding key and all receive a copy. ```java BindingBuilder.bind(queue).to(new DirectExchange("orders.x")).with("orders.created"); ``` Special case: the **default exchange** (empty-string name, a built-in DirectExchange) auto-binds every queue by its own name, which is how `rabbitTemplate.convertAndSend("orders.q", msg)` reaches a queue without any explicit binding. ### TopicExchange Interprets both routing keys and binding keys as **dot-delimited words** and matches with two wildcards: - `*` matches **exactly one** word. - `#` matches **zero or more** words. Examples with binding `order.*.eu`: matches `order.created.eu`, not `order.eu` or `order.created.updated.eu`. Binding `order.#` matches `order`, `order.created`, `order.created.eu`, etc. This makes topic exchanges the workhorse for hierarchical/topical event streams. ```java BindingBuilder.bind(queue).to(new TopicExchange("events.x")).with("order.*.eu"); ``` ### FanoutExchange **Ignores the routing key completely** and delivers a copy of every message to **every** bound queue — classic publish/subscribe / broadcast. The binding takes no key: ```java BindingBuilder.bind(queue).to(new FanoutExchange("broadcast.x")); // no .with(...) ``` Calling `.with("anything")` on a fanout binding has no effect on routing. ### HeadersExchange (the fourth, less common) Routes on **message header values** rather than the routing key, using an `x-match` argument of `all` (AND) or `any` (OR). Built with `BindingBuilder.bind(queue).to(headersExchange).where("key").matches("value")` or `.whereAll(map).match()`. ## Choosing a type - **Direct** — exact, small set of discrete routing keys. - **Topic** — hierarchical subjects, subscribers want subsets via patterns. - **Fanout** — every subscriber needs everything (broadcast, cache invalidation). - **Headers** — routing depends on structured metadata, not a single string. ## Gotchas - Topic `*` is **one word**, not one character — a common mix-up. `#` can match the empty set of words. - With Fanout, adding routing keys is a code smell; if you find yourself wanting selectivity, you actually want Topic or Direct. - The exchange type is fixed at declaration; you cannot change an existing exchange's type — you must delete and redeclare (or use a new name), or you'll hit a PRECONDITION_FAILED on mismatch. - Binding to a **non-durable** exchange from a durable queue, or type mismatches versus what's already on the broker, cause redeclaration failures.

  • Does the binding key 'order.*' match 'order.created.eu' on a topic exchange?
    No. '*' matches exactly one word, so 'order.*' matches 'order.created' but not 'order.created.eu'. Use 'order.#' to match one-or-more (actually zero-or-more) trailing words.
  • You need every consumer to get every message. Which exchange, and what happens if you add a routing key to the binding?
    FanoutExchange. A routing key on a fanout binding is ignored — routing is unconditional, so all bound queues receive a copy regardless of the key.

saying these in an interview costs you the question

  • Thinking topic '*' matches multiple words or any character sequence
  • Believing a fanout exchange respects the routing key
  • Claiming you can switch an existing exchange's type in place

context