Message Brokers, Queues & Integration Patterns
The state machines behind work queues and publish-subscribe: routing, publisher confirms, consumer acknowledgements, redelivery, ordering, backpressure, retries, dead letters, and the integration patterns that turn broker primitives into reliable application workflows.
A broker is not merely a network pipe between two processes. It is a state machine that decides where a message belongs, who currently owns it, when that ownership expires, and when the broker may forget it. Most production surprises come from leaving one of those decisions implicit.
The first design question is therefore not “RabbitMQ or Kafka?” It is whether the application needs a work queue whose delivery state is owned by the broker, a retained log whose read positions are independent state, or a combination. This entry develops the queue side. Log-Based Messaging & Streaming develops the other.
The broker state machine #
In the AMQP 0-9-1 model used by RabbitMQ, publishers do not write directly to queues. They publish to an exchange; bindings tell the exchange which queues receive a copy; consumers receive deliveries from a queue. The exchange routes but does not retain messages. Each destination queue owns its copy and its delivery state independently.
There are two independent responsibility transfers:
- Publisher → broker. A socket write proves only that bytes entered a local kernel buffer. A publisher confirm says that RabbitMQ accepted the publish according to the destination's durability rules. For a quorum queue, the confirm is issued only after a quorum of replicas accepted the message. A confirm does not say that any consumer saw or processed it.
- Broker → consumer. A consumer acknowledgement says that the application accepts responsibility for the delivery. The safe point is after the intended durable side effect commits, not when the handler starts and not merely when bytes arrive. An acknowledgement knows nothing about the original publisher.
Those signals are deliberately orthogonal. End-to-end business completion needs a domain acknowledgement or a queryable state transition of its own. Publisher confirms plus consumer acknowledgements provide the machinery for at-least-once processing; they do not make an arbitrary database update, email, and payment exactly once.
Channels, queues, and subscriptions are different objects #
A message carries an opaque body plus metadata. A channel is the logical path over which messages travel. A queue stores messages and assigns deliveries to consumers. A subscription is durable interest in a source; in queue-oriented brokers it is often implemented as a queue plus a binding.
This distinction explains the two common topologies:
| Intent | Topology | Ownership consequence |
|---|---|---|
| Divide work among workers | one queue, many competing consumers | each queued copy is assigned to one consumer at a time |
| Let independent services observe the same event | one source, one queue/subscription per service | routing creates an independently acknowledged copy per service |
Adding three consumers to one queue does not broadcast each message three times; it divides work. Conversely, binding three queues to a fanout or topic exchange creates three independently retained copies. This is the key design difference between “scale one logical subscriber” and “add another logical subscriber.”
AMQP's exchange types express common routing policies:
| Exchange | Match | Typical use |
|---|---|---|
| direct | binding key equals routing key | named command or work class |
| fanout | every binding matches | broadcast to independent queues |
| topic | dot-separated key with * and # wildcards |
event categories such as orders.*.failed |
| headers | selected header values match | routing on typed metadata rather than one string key |
The default RabbitMQ exchange is still a direct exchange: every declared queue is automatically bound with its queue name as routing key. Publishing “to a queue” is therefore convenient syntax over the same exchange/binding model, not a separate transport path.
Delivery semantics are failure policies #
The familiar labels are only meaningful after naming the boundary:
| Policy | Failure behavior | Application consequence |
|---|---|---|
| at-most-once | acknowledge before work, or do not retry ambiguous sends | loss is possible; duplicate processing is avoided only by dropping uncertainty |
| at-least-once | retain or resend until an acknowledgement arrives | duplicates are possible; handlers must be idempotent |
| exactly-once claim | deduplicate and atomically couple a narrowly defined read/write boundary | inspect the scope; external effects usually remain outside it |
Consider a worker that charges a card and then crashes before acknowledging. The broker cannot distinguish “charge committed, ack lost” from “charge never happened,” so it redelivers. Suppressing that retry would lose work in the second case. The broker cannot solve the ambiguity alone; the handler needs a stable message or command id and a durable idempotency record in the same transaction as the protected state change.
BEGIN;
INSERT INTO handled_messages (consumer, message_id)
VALUES ('billing', :message_id)
ON CONFLICT DO NOTHING;
-- Continue only when the insert affected one row.
UPDATE invoices
SET status = 'paid'
WHERE id = :invoice_id AND status = 'pending';
COMMIT;
-- Acknowledge the delivery only after COMMIT succeeds.This protects the database transition. An email or remote API call still needs its own idempotency key or an outbox/inbox boundary.
Ordering ends where parallel work begins #
A queue can dequeue in FIFO order while the application completes out of order. If worker A receives message 41 and pauses while worker B finishes 42, the observable side effects are 42 then 41. Prefetch makes this window larger: messages already delivered but not acknowledged are no longer available to other workers, even though they have not completed.
RabbitMQ's strongest simple ordering path is one publishing channel → one exchange → one queue → one active consumer, without priorities or requeueing. Multiple publishers can interleave; priorities intentionally reorder; redelivery can move an older message relative to newer work; multiple active consumers preserve dequeue order but not completion order.
When the domain requires order per entity but also needs parallelism,
partition the work by a stable key such as order_id across several queues,
then use one active consumer per partition. The key becomes a durable contract:
changing the partition count or hash function can move an entity and overlap
old and new workers, so migration needs an explicit drain or epoch boundary.
Prefetch is the local backpressure valve #
RabbitMQ normally pushes deliveries. Prefetch limits the number of unacknowledged deliveries assigned to a consumer. Too low and the worker starves while acknowledgements travel back; too high and one slow worker hoards work, increases redelivery bursts after a crash, and hides backlog in client memory rather than in observable queue depth.
For average processing time seconds and desired per-worker concurrency , a first approximation is prefetch near , then measure. The system has at least three distinct inventories:
Watching only ready messages can therefore report a healthy queue while
thousands of deliveries are stuck unacknowledged. Pair queue depth with oldest
message age, unacknowledged count, redelivery rate, handler latency, and
publisher-confirm latency. The broader capacity model belongs in
Backpressure & Load Shedding.
Retries are a scheduled control loop #
Immediate negative-acknowledge-and-requeue is often a positive feedback loop: the dependency is down, every worker fails instantly, and the same messages consume all broker and application capacity. A durable retry design includes:
- a bounded attempt count recorded in trustworthy metadata;
- exponential or otherwise increasing delay with jitter;
- classification of transient, permanent, and unknown failures;
- a separate retry schedule or delayed queue so fresh work is not starved;
- idempotency across every attempt;
- an operator-visible terminal state.
A dead-letter queue is quarantine, not recovery. It preserves evidence after expiry, rejection, or attempt exhaustion, but it does not decide whether the payload is poison, repair it, or safely replay it. Re-drive must preserve the original id, record the operator action, and avoid resetting an infinite retry cycle. RabbitMQ dead-lettering is itself a publish operation and can fail; default dead-letter transfer is not automatically lossless. Quorum queues provide an explicit at-least-once dead-letter mode using internal publisher confirms, with its documented constraints.
Enterprise Integration Patterns are compositions, not products #
Enterprise Integration Patterns is useful because it names recurring roles independently of RabbitMQ, Kafka, SQS, or a framework. The 65-pattern catalog is best understood as six questions:
| Family | Design question | Representative patterns |
|---|---|---|
| message construction | what does the message mean and how are conversations related? | Command Message, Event Message, Correlation Identifier, Message Sequence, Expiration |
| channels | which delivery relationship exists? | Point-to-Point Channel, Publish-Subscribe Channel, Dead Letter Channel, Message Bus |
| routing | which recipients and processing path apply? | Content-Based Router, Recipient List, Splitter, Aggregator, Resequencer, Scatter-Gather |
| transformation | how do incompatible representations meet? | Translator, Envelope Wrapper, Content Enricher, Normalizer, Canonical Data Model |
| endpoints | how does an application safely attach? | Polling Consumer, Event-Driven Consumer, Competing Consumers, Durable Subscriber, Idempotent Receiver |
| management | how is the invisible flow observed and controlled? | Control Bus, Wire Tap, Message History, Message Store, Test Message |
The patterns are a vocabulary, not a mandate to insert a broker or an enterprise service bus everywhere. A Content-Based Router implemented as a pure function inside one service is still the same design role. Distributing it adds independent failure, deployment, and observability boundaries; that cost must buy something concrete.
Worked flow: order fulfillment #
Suppose orders.accepted contains a list of line items:
- A Message Translator converts the producer schema into the fulfillment
contract and preserves
message_id,order_id, causation, and trace ids. - A Splitter emits one reservation command per line with sequence size and position metadata.
- Competing inventory workers act as Idempotent Receivers keyed by the command id.
- An Aggregator correlates replies by
order_id, persists partial state, and completes when all expected positions arrive or a deadline expires. - A Content-Based Router sends complete orders to shipping and incomplete ones to compensation.
- A Wire Tap copies lifecycle events to observability without becoming a dependency of the command path.
The hard parts are state and time: where the aggregator's partial state lives, what duplicate replies do, how late replies are classified, and what happens after the timeout. Merely drawing Splitter → Aggregator does not answer them.
Request-reply over a broker #
Messaging can emulate RPC by placing a reply address and correlation id in the request. That does not make the call non-blocking from the caller's point of view: the caller still needs a deadline, cancellation policy, bounded reply state, duplicate-response handling, and behavior for a late response after the request has timed out.
Use brokered request-reply when routing, temporal decoupling, buffering, or auditable commands are requirements. For a simple low-latency query where the caller cannot proceed without the answer, ordinary RPC is usually more honest and easier to operate.
Queue implementations and near alternatives #
Products overlap, so compare the semantics selected by configuration rather than the logo:
| System / mode | State model | Best fit | Important boundary |
|---|---|---|---|
| RabbitMQ quorum queue | routed queue; replicated ownership and ack state | reliable commands and work distribution with rich routing | confirms and manual acks are required to benefit from quorum durability |
| RabbitMQ stream | retained append-only log with offsets | replay and large backlogs inside the RabbitMQ ecosystem | not a traditional queue; several queue features such as DLX do not apply |
| ActiveMQ Artemis | addresses, queues, anycast/multicast routing; multiple protocols | JMS/AMQP environments and conventional broker workloads | protocol-facing names map onto Artemis's own address model |
| Amazon SQS | managed pull queue with visibility leases | operationally simple background work | Standard queues are at-least-once and best-effort ordered; delete closes the lease |
| Google Cloud Pub/Sub | topic plus independently retained subscriptions | managed fanout and elastic subscribers | default delivery is at-least-once and unordered; ordering/exactly-once have explicit scopes |
| Kafka / Redpanda / Pulsar | retained partitioned logs and subscription positions | replayable event history and stream processing | partition key and retention are data-model decisions, not tuning details |
RabbitMQ classic mirrored queues, discussed in older comparisons, are not a current high-availability recommendation: they were deprecated in 2021 and removed in RabbitMQ 4.0. Quorum queues are the replicated queue primitive; streams are the retained-log primitive. This is one reason a 2018 product comparison should not be used as an operational guide today.
Design review checklist #
Before choosing or shipping a broker topology, answer these explicitly:
- Is each message a command, fact, document, or reply? Who owns its schema?
- Does each logical subscriber need an independent copy, or are consumers dividing one workload?
- What exact event allows the publisher to forget the message?
- What durable effect must commit before the consumer acknowledges?
- Which duplicate ids and idempotency records survive process and broker restarts?
- Which ordering boundary matters, and what concurrency preserves it?
- Where are ready, unacknowledged, delayed, and dead-lettered inventories measured?
- Which failures are retried, after what delay, how many times, and by whom?
- Can unroutable publishes and failed dead-letter transfers disappear?
- How are schema evolution, correlation, causation, trace context, sensitive data, retention, and deletion governed?
- Is replay required? If yes, a queue may be the wrong source of truth.
If any answer is “the broker handles it,” the design is still underspecified.
Sources
- docsAdvanced Message Queuing Protocol Specification, Version 0-9-1pdfnotes
- bookEnterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutionsnotes
- docsRabbitMQ — Exchanges
- docsRabbitMQ — Consumer Acknowledgements and Publisher Confirms
- docsRabbitMQ — Quorum Queues
- docsRabbitMQ — Dead Letter Exchanges
- docsAmazon SQS — Standard Queues
- docsGoogle Cloud Pub/Sub — Subscription Overview
- docsApache ActiveMQ Artemis — Address Model