A common assumption is that you publish messages straight into a RabbitMQ queue. You don't. Every message goes to an exchange first. The exchange routes it, and one or more queues store it. That is the difference between an exchange and a queue: routing versus storage. Once you see that separation, the rest of RabbitMQ makes sense.
A producer publishes a message to an exchange, along with a routing key. The exchange holds no messages. It looks at the routing key and its bindings, then forwards the message to the matching queues.
RabbitMQ has four exchange types, and they differ only in how they match:
* matches one word, a # matches zero or more. A binding key of orders.*.created catches orders.eu.created and orders.us.created.Choosing between the exchange types comes down to two questions. Direct vs topic: do you need wildcards in your routing? Topic vs fanout: do consumers need to filter messages, or does everyone get everything?
There is also a default exchange with no name. When a client appears to publish "directly to a queue", it is actually publishing to the default exchange, which routes by queue name. So even the simplest setup goes through an exchange.
A queue is where messages wait. It delivers them in first-in, first-out order to whichever consumer is subscribed.
Three queue features matter in practice:
Queues also come in types: classic, quorum, and stream. Quorum queues replicate messages across nodes, so they survive the loss of a broker, not just a restart. For most single-node setups, classic queues are fine.
The full path is short. The producer publishes a message to an exchange. The exchange checks its bindings and routes the message to zero or more queues. The message sits in each queue until a consumer picks it up and acknowledges it.
Bindings are the part you configure: a binding links a queue to an exchange, optionally with a binding key. Two terms sound alike here and trip people up. The routing key is set by the producer on each message. The binding key is set on the binding when you connect a queue to an exchange. The exchange compares one against the other. Routing behavior is not a property of the queue or the exchange alone; it comes from the bindings between them. For a real TypeScript implementation that puts this together, I built an RPC pattern with RabbitMQ that shows how producers, exchanges, and queues work end to end in a service-to-service scenario.
In RabbitMQ, exchanges are message routers that don't store messages, while queues are message storage units. Exchanges decide where messages go based on routing rules, and queues hold messages until they're processed by consumers. This separation allows for flexible and efficient message handling in distributed systems. If you're designing a larger architecture, this pattern fits naturally with CQRS and Event Sourcing, where the exchange/queue layer handles event routing between the command and query sides.
Occasional notes on software, tools, and things I learn. No spam.
Unsubscribe anytime.