Message Queue vs Topic (Publish‑Subscribe)
A beginner‑to‑production deep dive into two of the most fundamental messaging patterns in distributed systems — what they are, how they differ under the hood, and when to reach for each one, with Java / Spring Boot examples and real production war stories.
Introduction & History
If you have ever stood in line at a coffee shop, you have already lived the core idea behind a message queue. You take a token, the barista calls tokens one at a time, and each token is served exactly once by exactly one barista. Now imagine instead a radio station broadcasting the morning news — everyone with a radio tuned to that frequency hears the same broadcast at the same time, and it doesn’t matter whether one listener or ten thousand listeners are tuned in. That second scenario is the essence of a topic, also called the publish‑subscribe (pub‑sub) pattern.
Both patterns solve the same underlying problem — how do two pieces of software that don’t run at the same time, on the same machine, or even in the same country, exchange information reliably? — but they solve it with very different delivery semantics. Confusing the two, or picking the wrong one, is one of the most common architectural mistakes made by engineers moving from monoliths to distributed systems.
A brief history
Message‑oriented middleware (MOM) has been around since the late 1980s. IBM’s MQSeries (now IBM MQ), launched in 1993, popularized the point‑to‑point queue model for enterprise transaction processing — banks and airlines needed guaranteed, ordered, once‑only delivery of financial messages between mainframes. Around the same time, publish‑subscribe systems emerged from research into distributed event notification, with systems like TIB/Rendezvous (1994) built for Wall Street trading floors, where thousands of traders needed the same market‑data ticks simultaneously.
The 2000s brought open standards: JMS (Java Message Service) in 1999 unified the queue and topic abstractions under one Java API, and open‑source brokers like ActiveMQ and RabbitMQ (built on the AMQP protocol) made messaging accessible outside large enterprises. The real turning point came in 2011 when LinkedIn open‑sourced Apache Kafka — a distributed log‑based system that blurred the line between “queue” and “topic” by making topics horizontally partitioned and infinitely replayable, purpose‑built for the scale of modern web companies. Today, cloud providers offer both flavors as managed services: Amazon SQS (queue) and SNS (pub‑sub), Google Cloud Pub/Sub, Azure Service Bus (queues and topics in one product), and RabbitMQ or Kafka running in Kubernetes.
IBM MQSeries (Queues)
Popularized the point‑to‑point queue model for enterprise transaction processing on mainframes.
TIB / Rendezvous (Pub‑Sub)
Built for Wall Street trading floors where thousands of traders needed the same market‑data ticks simultaneously.
JMS Specification
The Java Message Service unified the queue and topic abstractions under one Java API.
AMQP protocol / RabbitMQ era begins
Open‑source, protocol‑based brokers make messaging accessible outside large enterprises.
Amazon SQS launched
First widely used managed cloud queue — effectively infinite scale, pay per request.
Apache Kafka open‑sourced
Distributed log‑based system that blurs the line between queue and topic and defines modern high‑throughput streaming.
Amazon SNS launched
Managed push‑based pub‑sub, commonly paired with SQS for cloud fan‑out.
Google Cloud Pub/Sub GA
Global, low‑latency, push‑or‑pull pub‑sub as a fully managed service.
Azure Service Bus Topics mainstream
Enterprise‑grade topics + filtered subscriptions become a standard building block on Azure.
A message queue is a ticket counter — one ticket, one customer, served once. A topic is a loudspeaker announcement — one message, heard by everyone listening. Both move information from a sender to a receiver, but “how many receivers get a copy” is the fundamental fork in the road.
Why this distinction still confuses experienced engineers
Part of the confusion is historical: JMS, the API that shaped a generation of Java developers’ mental model of messaging, literally named its two destination types Queue and Topic, baking the terminology directly into the language everyone uses. But the underlying implementations have since diverged wildly. A “topic” in Azure Service Bus behaves very differently under the hood from a “topic” in Kafka, even though both use the same word — Service Bus topics are push‑based with rules‑based filtering per subscription, while Kafka topics are pull‑based, partitioned, append‑only logs. Understanding the pattern conceptually, independent of any one vendor’s terminology, is what actually transfers between projects and companies.
It also doesn’t help that a single piece of software can expose both patterns from the same underlying engine. RabbitMQ is fundamentally a queue broker, but its exchange types let you build pub‑sub‑style fan‑out on top of queues. Kafka is fundamentally a distributed log, but it can be made to behave exactly like a work queue by putting every consumer instance into the same consumer group. The names on the box don’t always tell you which pattern you’re actually getting — the consumer group and routing configuration does.
The Problem & Motivation
Why not just have services call each other directly over HTTP or gRPC? In a small system, direct synchronous calls are simple and fine. But as systems grow, direct calls create three compounding problems.
Tight temporal coupling
If Service A calls Service B synchronously and B is down, slow, or overloaded, A is stuck waiting — or fails outright. This is called temporal coupling: both services must be up and healthy at the exact same moment for the interaction to succeed. In a system with dozens of services, the probability that all of them are simultaneously healthy drops fast, and a single slow dependency can cause cascading failures across the whole call graph.
The one‑to‑many broadcast problem
Suppose an e‑commerce checkout event needs to notify the inventory service, the email service, the analytics service, the fraud‑detection service, and a loyalty‑points service. If the checkout service calls all five synchronously, it now depends on the availability and latency of five other systems just to complete a purchase. Adding a sixth consumer next quarter means modifying the checkout service’s code again. This doesn’t scale organizationally or technically.
Load spikes and backpressure
Real‑world traffic is bursty — flash sales, breaking news, batch jobs kicking off at midnight. A downstream service sized for average load will fall over under peak load if it has to process everything synchronously and instantly. Systems need a way to absorb bursts and let consumers work through them at a sustainable pace.
Netflix’s video encoding pipeline receives uploads in unpredictable bursts (a studio might upload 200 hours of footage in one afternoon). Instead of encoding synchronously, uploads are placed onto queues, and a fleet of encoding workers pulls jobs at whatever rate they can sustain — smoothing a huge spike into steady, manageable work.
Message queues and topics both decouple producers from consumers in time and in code, but they decouple differently: a queue decouples a producer from a single logical consumer group doing work; a topic decouples a producer from an arbitrary, evolving set of independent subscribers who each care about the same event for different reasons. Understanding which decoupling problem you actually have is the first step to picking the right tool.
Core Concepts
Before comparing internals, let’s pin down the vocabulary. Nearly every argument over “queue vs topic” ends up being an argument about one of these five terms.
What is a Message Queue?
A message queue is a data structure and a piece of middleware that stores messages in the order they arrive (typically FIFO — First In, First Out) and delivers each message to exactly one consumer. Once a consumer successfully processes and acknowledges a message, it is removed from the queue. If you have multiple consumer instances reading from the same queue (a “consumer group” or “worker pool”), the queue’s messages are load‑balanced — spread across the pool — but each individual message is still handed to only one worker.
Think of a print queue on an office printer. Ten people send documents to the same printer. The printer prints them one at a time, in order. Once your document is printed, it’s removed from the queue — it doesn’t print twice, and it doesn’t print on someone else’s printer too.
What is a Topic (Publish‑Subscribe)?
A topic is a named channel to which producers publish messages, and to which any number of consumers can subscribe to receive a copy of every message published. Unlike a queue, a topic doesn’t “remove” a message after one consumer reads it — every subscriber gets its own independent copy (or, in log‑based systems like Kafka, its own independent read position, or “offset,” into a durable log).
Think of a YouTube channel. When a creator uploads (publishes) a new video, every subscriber gets notified — one subscriber watching the video doesn’t remove it or prevent another subscriber from watching it too. Everybody who is subscribed gets their own full copy of the notification.
The fundamental distinction: fan‑out cardinality
| Aspect | Message Queue | Topic (Pub‑Sub) |
|---|---|---|
| Delivery target | Exactly one consumer per message | Every subscriber gets a copy |
| Consumer relationship | Competing consumers (work‑sharing) | Independent, non‑competing subscribers |
| Typical use | Task distribution, job processing | Event broadcasting, notifications |
| Message lifecycle | Deleted / archived after ack | Retained per retention policy; re‑readable by new subscribers |
| Adding a new consumer | Increases processing throughput (shares existing work) | Adds a brand‑new, independent stream of the same events |
Producers, Consumers, Brokers — the vocabulary
- Producer (Publisher): the application that creates and sends a message. Why: it needs to tell the rest of the system that something happened or that work needs doing, without waiting for a response. Analogy: a newspaper’s editorial desk deciding to print a story.
- Consumer (Subscriber): the application that receives and processes a message. Why: it needs to react to events or perform work asynchronously. Analogy: a newspaper reader, or in a queue’s case, a single delivery boy assigned one paper route.
- Broker: the middleware server that stores messages and routes them from producers to consumers. Why: without a broker, producers and consumers would need to know about each other directly (tight coupling) and would need to be online at the same time. Analogy: the post office that sits between the sender and the recipient.
- Exchange / Router (in AMQP systems like RabbitMQ): decides which queue(s) a published message should be routed to, based on routing keys or patterns.
- Consumer Group: a named set of consumer instances that share the work of consuming from a queue or a partitioned topic; within a group, each message goes to only one member.
Delivery semantics: at‑most‑once, at‑least‑once, exactly‑once
Both queues and topics have to make a promise about how many times a message might be delivered, and that promise matters enormously for how you write consumer code.
- At‑most‑once: a message is delivered zero or one times — it might be lost, but it’s never duplicated. Why it exists: useful when losing an occasional message is cheaper than the complexity of guaranteeing delivery, e.g. non‑critical metrics. Example: a producer that fires‑and‑forgets a UDP‑based log shipper.
- At‑least‑once: a message is delivered one or more times — it’s never silently lost, but it may be duplicated during retries. Why it exists: this is the most common default for both queues and topics because guaranteeing zero loss without guaranteeing zero duplication is far cheaper to implement. Example: a consumer acknowledges a message just after processing it, but crashes before the ack reaches the broker — the broker redelivers, causing a duplicate.
- Exactly‑once: a message is processed as if it were delivered precisely one time, even though the underlying transport may retry. Why it exists: needed when duplicate processing would cause real harm, like double‑charging a customer. Example: Kafka’s idempotent producer plus transactional consumer‑producer chains achieve exactly‑once within Kafka; SQS FIFO queues achieve it through deduplication IDs within a 5‑minute window.
Imagine mailing a check, not hearing back for two weeks, and mailing a second check just in case the first got lost. If both arrive, the recipient’s bank might deposit both — a duplicate. That’s exactly the risk “at‑least‑once” delivery introduces, and it’s why idempotency on the receiving end (the bank refusing to deposit the same check number twice) is what actually makes the overall system safe.
Architecture & Components
The two patterns differ not just in delivery semantics but in what the broker actually stores and how it moves data around.
Queue architecture (e.g. RabbitMQ, Amazon SQS)
A typical queue‑based system has a broker holding one or more named queues. Producers publish directly to a queue (or via an exchange that routes to a queue). Consumers open a connection, pull or receive pushed messages, process them, and send an acknowledgment (ack) back to the broker. If no ack arrives within a visibility timeout, the broker assumes the consumer failed and redelivers the message to another consumer.
Topic architecture (e.g. Kafka, Amazon SNS, Google Pub/Sub)
A topic‑based system has a broker (or cluster of brokers) that maintains a topic as either (a) a fan‑out list of subscriber endpoints (SNS‑style — push model, messages aren’t retained after delivery attempts), or (b) a durable, partitioned, replayable log (Kafka‑style — pull model, each subscriber tracks its own offset / position). In the Kafka model, a topic is split into partitions for parallelism, and each partition is an append‑only, ordered log replicated across multiple broker nodes.
Core components compared
Queue — typical components
- Broker (RabbitMQ node, SQS service)
- Named queue(s), often with dead‑letter queues (DLQ)
- Exchanges / routing keys (AMQP)
- Consumer pool (competing consumers)
- Visibility timeout / ack mechanism
Topic — typical components
- Broker cluster (Kafka brokers, SNS service)
- Topic, split into partitions (log‑based) or subscription list (push‑based)
- Subscriptions (each with its own filter / endpoint)
- Consumer groups per subscription (log‑based)
- Offsets / cursors per consumer group
Java / Spring Boot: declaring a queue vs a topic
// --- RabbitMQ Queue declaration (Spring AMQP) ---
@Bean
public Queue orderProcessingQueue() {
return QueueBuilder.durable("order.processing.queue")
.withArgument("x-dead-letter-exchange", "order.dlx")
.build();
}
// --- Kafka Topic declaration (Spring Kafka) ---
@Bean
public NewTopic orderEventsTopic() {
return TopicBuilder.name("order-events")
.partitions(6)
.replicas(3)
.config("retention.ms", "604800000") // 7 days
.build();
}Protocols underneath the abstractions
It helps to know what’s actually on the wire. AMQP (Advanced Message Queuing Protocol), used by RabbitMQ, defines exchanges, bindings, and queues as first‑class protocol concepts — a publisher never sends directly to a queue, it always sends to an exchange, which then routes the message to zero, one, or many bound queues based on a routing key and the exchange type (direct, fanout, topic, or headers). This is actually how RabbitMQ implements pub‑sub: a fanout exchange routes one published message into every queue bound to it, and each of those queues can then have its own independent consumer or pool of consumers.
Kafka doesn’t use AMQP at all — it has its own binary wire protocol optimized for high‑throughput sequential writes and reads, built around the partitioned commit log rather than exchange‑based routing. Cloud‑native systems like SQS and SNS use plain HTTPS / REST APIs rather than a persistent binary protocol, trading a small amount of latency for operational simplicity and effectively infinite managed scalability.
Subscriptions vs consumer groups
In push‑based pub‑sub systems (SNS, Azure Service Bus Topics, Google Pub/Sub push mode), a subscription is a durable, named registration that says “deliver every message on this topic to this specific endpoint,” and each subscription can additionally apply a filter so it only receives a relevant subset of messages. In pull‑based log systems (Kafka), the nearest equivalent is a consumer group — a named identity that tracks its own offset per partition. The practical difference: a Service Bus subscription is closer to “a durable mailbox with a filter,” while a Kafka consumer group is closer to “a bookmark into a shared, replayable book.”
Internal Working
How the broker actually keeps its promise — the low‑level mechanics behind acks, offsets, and ordering.
How a queue delivers a message exactly‑once‑per‑consumer
Internally, a queue broker maintains an ordered list of undelivered messages. When a consumer connects, the broker hands it the next available message and marks it “in‑flight” (invisible to other consumers) rather than deleting it immediately. This is crucial: if the consumer crashes mid‑processing, the broker needs to know to redeliver the message rather than lose it. Only once the consumer sends an explicit acknowledgment does the broker permanently delete the message. If the ack doesn’t arrive before a timeout (e.g. SQS’s VisibilityTimeout), the message becomes visible again and another consumer can pick it up — this is what enables horizontal scaling of workers pulling from one queue.
How a topic fans a message out to many subscribers
In push‑based systems (SNS, Google Pub/Sub push mode), the broker itself calls each subscriber’s endpoint (HTTP webhook, Lambda, SQS queue, etc.) for every message — the broker tracks per‑subscriber delivery success and retries independently. In pull‑based, log‑structured systems (Kafka), the broker doesn’t “push” anything; instead every message published to a topic partition is appended to a persistent, ordered log file on disk. Each consumer group independently tracks an offset — a pointer to “how far into the log has this group read.” Multiple consumer groups can read the very same log at completely different speeds and positions without interfering with each other, because reading doesn’t delete anything.
Producer appends msg@100, msg@101, msg@102. Group A is currently at offset 99, catches up, and commits offset 102. Group B is still at offset 50 and slowly reads through msg@50 ... msg@102 at its own pace — neither group knows or cares what the other is doing.
Ordering guarantees, internally
A single queue typically guarantees FIFO order only if configured as a FIFO queue (e.g. SQS FIFO) — standard queues trade strict ordering for higher throughput and may deliver out of order. A Kafka topic guarantees ordering only within a single partition; across partitions there is no global order. This is why messages that must stay ordered relative to each other (e.g. all events for a given orderId) are published with the same partition key, ensuring they always land in the same partition.
Java: manual acknowledgment internals
@RabbitListener(queues = "order.processing.queue", ackMode = "MANUAL")
public void handleOrder(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
processOrder(message);
channel.basicAck(deliveryTag, false); // acknowledge success
} catch (RecoverableException e) {
channel.basicNack(deliveryTag, false, true); // requeue for retry
} catch (Exception e) {
channel.basicNack(deliveryTag, false, false); // send to DLQ
}
}
@KafkaListener(topics = "order-events", groupId = "inventory-service")
public void onOrderEvent(ConsumerRecord<String, String> record, Acknowledgment ack) {
processEvent(record.value());
ack.acknowledge(); // commits the offset for this consumer group only
}Data Flow & Lifecycle
A message’s life is short in a queue and much longer in a topic — the shape of that life is what makes each pattern good at different jobs.
Lifecycle of a message in a queue
- Produced: Producer sends message to queue; broker persists it (usually to disk for durability).
- Enqueued: Message sits in the queue, in order of arrival (or priority, if a priority queue).
- Delivered / Locked: Broker hands the message to an available consumer and marks it invisible to others.
- Processed: Consumer runs business logic.
- Acknowledged or Rejected: On success, the broker deletes the message. On failure, it becomes visible again (retry) or is routed to a Dead‑Letter Queue (DLQ) after N failed attempts.
- Terminal state: Deleted (success) or archived in DLQ (permanent failure) for manual inspection.
Lifecycle of a message in a topic
- Published: Producer sends message to a topic (optionally with a partition key).
- Appended / Fanned out: In a log‑based system, the message is appended to the partition’s log; in a push‑based system, the broker immediately attempts delivery to every subscriber.
- Retained: The message stays available per the topic’s retention policy (e.g. 7 days, or “until log compaction removes an older value for the same key”) — regardless of whether anyone has consumed it yet.
- Consumed independently: Each subscriber (or consumer group) reads the message on its own schedule, tracking its own offset / cursor.
- Never “removed” by consumption: Unlike a queue, one subscriber reading the message does not affect any other subscriber’s ability to read it.
- Expiration: Eventually the message ages out per the retention policy, independent of whether it was ever read.
A queue message’s lifecycle is like a single certified letter — once delivered and signed for, it’s gone. A topic message’s lifecycle is like a newspaper archive — it sits on the shelf for a set number of days, and any number of readers can come pick up their own copy whenever they want, without affecting the shelf copy.
Data flow diagram: an e‑commerce checkout event
Notice how the topic fans one event out to three independent teams’ services, while the queue inside the inventory service distributes reservation tasks across a pool of workers — each task handled exactly once. This combination — topics for broadcasting facts, queues for distributing work — is extremely common in production systems.
Pros, Cons & Trade‑offs
Every design choice trades one property for another. Here is the full ledger for both patterns.
Queue — Pros
- Simple mental model: one job, one worker
- Natural load balancing across a worker pool
- Easy to reason about “exactly this piece of work happened once”
- Built‑in retry and dead‑letter handling in most implementations
- Lower operational complexity for simple task distribution
Queue — Cons
- Not designed for broadcasting to multiple independent consumers
- Adding a new “listener” for the same event usually means adding a new queue and duplicating the publish, or bolting on a fan‑out exchange
- Once consumed, data is gone — no replay for a fresh consumer joining later
- Strict ordering across a whole queue can limit horizontal scalability
Topic — Pros
- Effortlessly supports many independent consumers of the same event
- New subscribers can be added without touching the producer
- Log‑based topics (Kafka) support replay — a new service can reprocess history
- Naturally models “event‑driven architecture” and domain events
- Partitioning provides high throughput with per‑key ordering
Topic — Cons
- More complex mental model — “who consumed what” is per‑subscriber state
- Global ordering across a topic is not guaranteed (only per‑partition)
- Operationally heavier (Kafka clusters need ZooKeeper / KRaft, partition rebalancing, disk management)
- Easy to accidentally create tight coupling via too many fine‑grained event types
- Retention / storage costs to keep replayable history
The trade‑off in one sentence
A queue trades broadcast flexibility for simple, guaranteed, load‑balanced work distribution; a topic trades some ordering and operational simplicity for the ability to notify an unbounded, evolving set of independent consumers about the same fact.
Performance & Scalability
Both patterns scale, but they scale along different axes and hit different ceilings.
Scaling a queue
Queues scale horizontally by adding more consumer instances to the same consumer pool — the broker automatically load‑balances new work across however many workers are online. Throughput is typically bounded by the broker’s ability to track in‑flight messages and by how fast the underlying storage (memory or disk) can persist and delete entries. RabbitMQ, being primarily memory‑resident with disk backing, favors low latency (sub‑millisecond to a few ms) at moderate throughput (tens of thousands of messages/sec per node). Amazon SQS abstracts this entirely and scales near‑infinitely as a managed service, at the cost of higher per‑message latency (tens of milliseconds).
Scaling a topic
Topics scale primarily through partitioning. A Kafka topic with 12 partitions can be consumed in parallel by up to 12 consumer instances within one consumer group (each partition assigned to exactly one consumer in the group at a time). More partitions mean more parallelism, but also more overhead (open file handles, replication traffic, longer rebalance times). Kafka is designed for very high sustained throughput — millions of messages/sec across a cluster — because it relies on sequential disk I/O and OS page cache rather than per‑message bookkeeping.
| Dimension | Queue (RabbitMQ / SQS) | Topic (Kafka / SNS) |
|---|---|---|
| Typical latency | Sub‑ms to ~20ms | 2ms (Kafka) to ~100ms (SNS push) |
| Peak throughput / node | 10K–50K msg/sec | Kafka: 100K–1M+ msg/sec |
| Scale‑out mechanism | Add consumers to pool | Add partitions + consumers |
| Storage pattern | Delete‑on‑ack (small footprint) | Append‑only log (larger footprint, replay capability) |
LinkedIn — Kafka’s birthplace — processes over 7 trillion messages per day across its Kafka infrastructure, powering everything from activity feeds to metrics pipelines, precisely because the partitioned‑log model scales horizontally by adding brokers and partitions rather than by re‑architecting.
Backpressure and consumer lag
In queue systems, backpressure shows up as a growing queue depth — if producers outpace consumers, the queue simply gets longer, which can be monitored and alerted on. In topic systems, the equivalent metric is consumer lag — the difference between the latest offset in the log and the offset a consumer group has committed. Both are the single most important operational health signal for a messaging system, because they tell you, in real time, whether your consumers are keeping up with your producers.
Why sequential disk I/O makes log‑based topics so fast
A counter‑intuitive fact about Kafka’s performance is that it’s built around writing to disk on every message, yet still achieves extremely high throughput — the trick is that it never does random‑access disk seeks. Every write is an append to the end of a log segment file, which is about as fast as writing to memory on modern hardware because the OS page cache absorbs most of the actual I/O, and sequential reads for consumers benefit from the same OS‑level read‑ahead. RabbitMQ, by contrast, was originally designed around message deletion after acknowledgment, which involves more random‑access bookkeeping — great for moderate‑throughput, low‑latency task queues, but not built to sustain Kafka‑scale sequential streaming workloads.
Tuning knobs that matter in practice
| Knob | Queue equivalent | Topic equivalent |
|---|---|---|
| Batch size | Prefetch count (RabbitMQ QoS) | Producer batch.size / linger.ms |
| Concurrency | Number of consumer threads / instances | Number of partitions × consumers per group |
| Durability vs speed | Persistent vs transient messages | acks=0/1/all |
| Compression | Payload‑level (application responsibility) | Built‑in (gzip, snappy, lz4, zstd) |
High Availability & Reliability
A broker outage cascades everywhere, so replication and failover are first‑class concerns for any production messaging deployment.
Replication
Both patterns achieve durability and availability through replication, but the unit of replication differs. RabbitMQ uses mirrored (classic) or quorum queues, where each queue’s messages are replicated across multiple broker nodes using a Raft‑based consensus protocol (quorum queues) — if the leader node dies, a follower with an up‑to‑date copy is elected leader. Kafka replicates at the partition level: each partition has one leader broker and N−1 follower (replica) brokers; producers write to the leader, followers replicate asynchronously (or synchronously depending on acks config), and if the leader fails, a follower that was “in‑sync” (ISR — In‑Sync Replica) is elected.
Durability configuration in Java
// Kafka producer: wait for all in-sync replicas to acknowledge before considering the write successful
Properties props = new Properties();
props.put(ProducerConfig.ACKS_CONFIG, "all");
props.put(ProducerConfig.RETRIES_CONFIG, 5);
props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true); // prevents duplicate writes on retry
// RabbitMQ: mark messages persistent so they survive a broker restart
MessageProperties props2 = MessagePropertiesBuilder.newInstance()
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();Failover and disaster recovery
Cloud‑managed services (SQS, SNS, Google Pub/Sub) replicate across multiple Availability Zones automatically as part of the SLA, so failover is invisible to the application. Self‑hosted clusters (RabbitMQ, Kafka) require deliberate multi‑AZ or multi‑region deployment: Kafka commonly uses MirrorMaker 2 or Confluent’s Cluster Linking to replicate topics across regions for disaster recovery, while RabbitMQ uses federation or the Shovel plugin. In both patterns, the key reliability question to ask is: “if this broker node disappears right now, how many committed messages could I lose, and how long until the system is serving traffic again?” — driving decisions about replication factor and min.insync.replicas.
Imagine three photocopies of an important contract kept in three different office buildings. If one building burns down, you still have two valid copies elsewhere. That’s exactly what a replication factor of 3 does for a Kafka partition or a RabbitMQ quorum queue.
Security
Messaging systems sit at the heart of a distributed system and often carry sensitive business data (orders, payments, personal information), so securing them matters as much as securing a database.
Authentication & Authorization
- SASL / SCRAM or mTLS for Kafka client authentication, so only known, credentialed services can produce or consume.
- ACLs (Access Control Lists) restricting which principals can produce to or consume from specific topics / queues — e.g. only the payments service can publish to
payment-events. - IAM policies for AWS SQS / SNS, scoping which roles can
sqs:SendMessageorsqs:ReceiveMessageon a specific queue ARN.
Encryption
- In transit: TLS between producers / consumers and brokers (mandatory in any production deployment handling real user data).
- At rest: disk‑level encryption for broker storage — SQS and Kafka both support server‑side encryption using KMS‑managed keys.
Java: securing a Kafka producer
Properties props = new Properties();
props.put("security.protocol", "SASL_SSL");
props.put("sasl.mechanism", "SCRAM-SHA-512");
props.put("sasl.jaas.config",
"org.apache.kafka.common.security.scram.ScramLoginModule required " +
"username="order-service" password="${env:KAFKA_PASSWORD}";");Data‑level concerns
Beyond transport security, teams increasingly apply field‑level encryption or tokenization to sensitive payload fields (e.g. masking a card number before it ever reaches the topic), and use schema validation (Avro / Protobuf with a schema registry) to prevent malformed or malicious payloads from silently breaking downstream consumers. Multi‑tenant systems must also guard against “topic squatting” or unauthorized subscription — a service that can subscribe to a topic it shouldn’t have access to is effectively a data leak.
Queue‑specific vs topic‑specific security concerns
Queues have a narrower attack surface because there’s usually a known, small set of consumers pulling from a specific named queue — access control is largely “who can send, who can receive, on this one queue.” Topics have a broader and more dynamic attack surface precisely because their whole value proposition is letting new, sometimes unknown subscribers attach over time; a mature platform team treats every new topic subscription request the way they’d treat a new API consumer requesting access to a sensitive endpoint — with an approval workflow, not just a self‑service button. In Kafka specifically, teams also need to secure the schema registry and any Kafka Connect connectors, since a compromised connector can silently exfiltrate an entire topic’s history to an external system.
Monitoring, Logging & Metrics
You cannot run a messaging system on vibes. The right handful of metrics tell you almost everything you need to know about health.
The metrics that actually matter
| Metric | Queue meaning | Topic meaning |
|---|---|---|
| Depth / Lag | Number of unprocessed messages in queue | Consumer group lag (offset gap) per partition |
| Throughput | Messages enqueued / dequeued per second | Messages produced / consumed per second, per partition |
| Age of oldest message | How long the oldest waiting message has sat | Time since the oldest un‑consumed offset was written |
| DLQ size | Count of permanently failed messages | Count of messages sent to an error / DLQ topic |
| Broker health | Node CPU, memory, disk, connection count | Broker under‑replicated partitions, ISR shrink events |
Distributed tracing
Because messaging breaks the direct call chain, distributed tracing tools (OpenTelemetry, Jaeger, Zipkin) propagate a correlation ID / trace context inside message headers, so a single business transaction can be traced end‑to‑end even as it hops across multiple queues and topics and services.
// Propagating a correlation ID through Kafka headers
ProducerRecord<String, String> record = new ProducerRecord<>("order-events", orderId, payload);
record.headers().add("X-Correlation-Id", correlationId.getBytes(StandardCharsets.UTF_8));
kafkaTemplate.send(record);
@KafkaListener(topics = "order-events")
public void listen(ConsumerRecord<String, String> record) {
String correlationId = new String(record.headers().lastHeader("X-Correlation-Id").value());
MDC.put("correlationId", correlationId); // attach to all downstream log lines
...
}Alerting philosophy
The single most valuable alert in any messaging system is on growing lag / depth combined with stable or falling consumption rate — this signals a stuck or crashed consumer before it turns into a full‑blown outage. Alerting purely on “queue depth > X” without considering trend and consumption rate produces noisy, low‑signal alerts that teams learn to ignore.
Deployment & Cloud
Where does the broker actually live? The answer shapes both operating cost and operator sanity.
Managed vs self‑hosted
Managed queues
- Amazon SQS — standard & FIFO queues, serverless, pay‑per‑request
- Azure Service Bus Queues — enterprise features (sessions, dedup)
- Google Cloud Tasks — HTTP‑target task queues
Managed topics
- Amazon SNS — push‑based fan‑out, integrates with SQS / Lambda
- Google Cloud Pub/Sub — global, push or pull
- Confluent Cloud / AWS MSK — managed Kafka
- Azure Service Bus Topics — topics + subscriptions with filters
Self‑hosted on Kubernetes
Self‑hosting RabbitMQ or Kafka on Kubernetes (via the Strimzi or Confluent operators for Kafka, or the RabbitMQ Cluster Operator) gives full control over configuration, retention, and cost, at the price of operational burden — StatefulSets with persistent volumes, careful broker anti‑affinity across nodes / zones, and manual upgrade orchestration for rolling restarts without data loss.
A Strimzi‑managed Kafka cluster runs three broker pods as a StatefulSet, each with its own PersistentVolumeClaim, spread across three availability zones with anti‑affinity, and a Spring Boot application uses the pod DNS as its bootstrap.servers list.
CI / CD considerations
Topic / queue schemas and configurations (partition counts, retention, ACLs) should be version‑controlled and applied through infrastructure‑as‑code (Terraform providers exist for Kafka topics, SQS queues, and SNS topics) rather than created ad hoc through a console — this avoids drift between environments and makes topic / queue changes auditable and reviewable like any other code change.
Databases, Caching & Load Balancing
Messaging never lives in isolation — the same events that flow across a topic almost always cross a database and a cache too.
Messaging and databases: the outbox pattern
A classic bug is updating a database and publishing an event as two separate operations — if the process crashes between them, the database and the event stream disagree. The transactional outbox pattern solves this: write both the business row and an “outbox” row in the same local database transaction, then a separate relay process reads the outbox table and reliably publishes to the queue or topic, deleting the outbox row only after successful publish.
Caching consumer state
High‑throughput consumers often cache reference data (e.g. product catalog, pricing) in Redis so that processing a message doesn’t require a database round‑trip per event — critical when a topic partition is delivering tens of thousands of messages per second and a naive per‑message DB lookup would become the bottleneck.
Load balancing consumers
For queues, load balancing is built into the broker — it simply hands the next message to whichever consumer asks next. For partitioned topics, “load balancing” happens through partition assignment: Kafka’s consumer group protocol assigns each partition to exactly one consumer instance in the group, and rebalances assignments when instances join or leave (e.g. during a rolling deployment or autoscaling event).
APIs & Microservices
Sooner or later every microservices architecture ends up with a mix of synchronous APIs and asynchronous messaging. Knowing which side of the line each interaction lives on is a large part of the job.
Where messaging fits in a microservices architecture
In a microservices system, synchronous APIs (REST, gRPC) are typically used for request / response interactions where the caller needs an immediate answer (e.g. “what’s the price of this item right now?”), while messaging is used for everything that is fundamentally about something having happened or work to be done later. Queues fit naturally where one service needs a group of workers to process a backlog of tasks (image resizing, PDF generation, sending a single email). Topics fit naturally where a domain event needs to reach several unrelated services that each react differently (order placed → inventory reserves stock, billing charges the card, email sends a receipt).
API Gateway and messaging
An API Gateway typically remains synchronous at the edge — the client’s HTTP request still gets an HTTP response — but internally the gateway or the first service it calls may immediately publish an event or enqueue a task and return a “202 Accepted” style response, deferring the heavy lifting to asynchronous processing behind the scenes.
@RestController
@RequestMapping("/orders")
public class OrderController {
private final KafkaTemplate<String, OrderEvent> kafkaTemplate;
private final AmqpTemplate rabbitTemplate;
@PostMapping
public ResponseEntity<OrderResponse> placeOrder(@RequestBody OrderRequest request) {
Order order = orderService.createOrder(request); // synchronous, fast write
// Broadcast the fact that an order happened (topic - many interested parties)
kafkaTemplate.send("order-events", order.getId(), new OrderEvent(order));
// Enqueue the actual fulfillment work (queue - one worker pool handles it)
rabbitTemplate.convertAndSend("fulfillment.queue", new FulfillmentTask(order.getId()));
return ResponseEntity.accepted().body(new OrderResponse(order.getId(), "PROCESSING"));
}
}Contract evolution
Because a topic may have many independent, unknown‑to‑the‑producer consumers, message schemas need the same discipline as public APIs — additive, backward‑compatible changes managed through a schema registry (Confluent Schema Registry, AWS Glue Schema Registry) with Avro or Protobuf, so a producer can evolve a payload without breaking a consumer team that hasn’t upgraded yet.
Design Patterns & Anti‑Patterns
The same handful of shapes appear over and over in real messaging systems — some of them helpfully, some of them as scars from previous mistakes.
CQRS and event sourcing, briefly
Two related patterns lean heavily on the topic model specifically. Event Sourcing stores every state change as an immutable event on a durable, replayable log (a natural fit for a Kafka‑style topic) rather than storing only the current state — the current state is derived by replaying events. CQRS (Command Query Responsibility Segregation) often pairs with event sourcing by having write operations publish events to a topic, and one or more independent “read model” services subscribe to that topic to build denormalized, query‑optimized views — each subscriber can build a completely different projection of the same underlying event stream without touching the write path at all. Both patterns are difficult or impossible to build cleanly on a queue, because a queue’s “delete on ack” lifecycle destroys the very history these patterns depend on being able to replay.
Useful patterns
Competing Consumers
Multiple worker instances pull from one queue to scale throughput horizontally — the textbook use of a queue.
Fan‑out
One topic feeds several independent downstream queues / services — the textbook use of a topic, often implemented as SNS‑fans‑out‑to‑multiple‑SQS‑queues in AWS.
Dead‑Letter Queue (DLQ)
Messages that repeatedly fail processing are routed aside for manual inspection instead of blocking or looping forever.
Saga (Choreography)
A sequence of local transactions coordinated purely through published domain events on topics, each service reacting to the previous step’s event.
Priority Queue
Using multiple queues or message priority fields so urgent work is processed ahead of routine work.
Log Compaction
A Kafka‑specific pattern where only the latest message per key is retained, turning a topic into a durable “changelog” of current state (used heavily for CDC — Change Data Capture).
Anti‑patterns to avoid
Publishing the same message to five separate queues from application code (one per interested consumer) recreates a fragile, hardcoded fan‑out that a real topic gives you for free — and it means the producer must know about every future consumer, defeating the point of decoupling.
Publishing individual units of work to a topic and having multiple independent instances of the same service all subscribe without a shared consumer group means every instance processes every task — duplicating work instead of sharing it. If you want load‑shared task processing, use one consumer group (which behaves like a queue) or an actual queue.
Cramming every kind of event across the whole company into one topic makes filtering, security, schema evolution, and ownership impossible to reason about. Prefer topics scoped to a bounded context or domain event type.
Both queues and topics can, under failure and retry, deliver the same message more than once (“at‑least‑once” delivery is the common default). A consumer that isn’t idempotent — e.g. charges a credit card again on redelivery — will eventually cause real damage. Consumers must be built to safely process the same message twice.
Best Practices & Common Mistakes
A short, opinionated checklist collected from real production incidents — the “do this” column and the “stop doing this” column of nearly every messaging post‑mortem.
Best practices
- Make consumers idempotent. Track processed message IDs (or use natural idempotency keys) so redelivery is safe.
- Choose partition / routing keys deliberately. Use a business identifier (e.g.
customerIdororderId) as the Kafka partition key when relative ordering matters for that entity. - Set sane retention and DLQ policies. Don’t let failed messages loop forever; don’t retain topic data indefinitely without a cost / compliance reason.
- Version your message schemas and use a schema registry for anything with more than one consumer team.
- Monitor lag / depth as a first‑class SLO, not an afterthought.
- Right‑size partition / queue counts — too few limits parallelism; too many adds rebalance and coordination overhead.
- Document event contracts the same way you’d document a public REST API — consumers you don’t know about are depending on you.
Common mistakes
- Assuming a queue or topic guarantees exactly‑once delivery by default — most systems are at‑least‑once unless deliberately configured (Kafka’s idempotent producer + transactional consumer, or SQS FIFO with dedup) for exactly‑once semantics.
- Forgetting that Kafka only orders messages within a partition, then being surprised when cross‑partition ordering breaks a business assumption.
- Not setting a visibility timeout (queues) or session / heartbeat timeout (topics) appropriately for how long processing actually takes, causing premature redelivery and duplicate processing.
- Treating a topic’s retention window as permanent storage — it is not a database and should not be the system of record.
- Ignoring poison messages — one malformed message that always throws an exception can block an entire partition or queue if not routed to a DLQ.
Real‑World / Industry Examples
Every one of the companies below runs at a scale where getting the pattern wrong shows up in the news — and every one of them uses both patterns side by side.
Uber — dispatch + trip events
Uber’s matching system distributes ride requests to nearby available drivers using queue‑like task distribution so exactly one driver is matched per ride, while trip lifecycle events (ride requested, driver assigned, trip started, trip completed) are published to Kafka topics consumed independently by pricing, fraud detection, and analytics teams.
Amazon — order fulfillment
Amazon’s order pipeline uses SQS queues to distribute warehouse picking and packing tasks across worker processes (each task done exactly once), while SNS topics broadcast order status changes to notification services, seller dashboards, and logistics partners simultaneously.
Netflix — telemetry & personalization
Netflix streams billions of playback events per day through Kafka topics; the same topic is independently consumed by the recommendation engine, the billing / usage system, and the A/B testing platform — each team reading the same stream of truth for entirely different purposes, without coordinating with each other or the producer.
Slack — message delivery
Slack uses queue‑based job processing (built on technologies like Kafka‑backed queues) to fan out message‑delivery, search‑indexing, and notification tasks reliably at massive scale, ensuring a message send doesn’t block on every downstream side effect completing synchronously.
LinkedIn — birthplace of the blur
LinkedIn built Kafka specifically because its existing queue‑based systems couldn’t cheaply support the same event (a profile view, a connection request, a feed update) being consumed independently by many different internal teams — search indexing, recommendations, monitoring, and the news feed all needed their own copy of the same activity stream at their own pace.
Cross‑cutting insight
Rather than maintaining dozens of point‑to‑point queues, LinkedIn’s engineers built a single durable, replayable log abstraction that any number of teams could subscribe to independently — which is precisely the topic pattern this guide describes, and it’s the direct ancestor of how most large‑scale event‑driven architectures are built today.
Every one of these companies uses both patterns side by side: queues for exactly‑once work distribution to a worker pool, and topics for broadcasting facts to many independent, evolving consumers. Neither pattern replaces the other — they answer different questions.
FAQ & Summary
A last set of questions we hear repeatedly, followed by a distilled summary and the key takeaways from the entire guide.
Can Kafka be used as a message queue?
Yes — if all consumers of a topic belong to the same consumer group, Kafka behaves like a load‑balanced queue, because each partition is only assigned to one member of that group at a time. Kafka topics are flexible enough to model both patterns depending on consumer group configuration.
Is RabbitMQ capable of pub‑sub?
Yes — using a fanout or topic exchange, RabbitMQ can route one published message to multiple bound queues, giving pub‑sub‑style fan‑out on top of its queue primitives.
Which one should I use for my microservices project?
Ask: “does this message represent work to be done exactly once by any available worker (queue), or a fact that multiple independent parts of the system need to know about (topic)?” That question alone resolves the vast majority of real design decisions.
Do topics guarantee message order?
Only within a single partition. If global ordering across an entire topic matters, you either need a single‑partition topic (limiting throughput) or you need to redesign so ordering is only required per‑key, then partition by that key.
What happens if a consumer is offline for a while?
For a queue, messages simply accumulate until a consumer comes back (subject to any max retention / TTL). For a topic, a consumer group’s committed offset stays where it was, and when the group resumes, it continues reading from that offset — as long as the messages haven’t aged out of the retention window.
Can I have multiple worker pools consuming the same topic independently, like multiple queues?
Yes — this is one of the most powerful features of log‑based topics. Give two different worker pools two different consumer group IDs, and each group gets its own independent, load‑balanced view of the same topic, effectively giving you the fan‑out of pub‑sub and the load‑balancing of a queue at the same time, per group.
How do I choose the number of partitions for a Kafka topic?
Start from your target throughput and your maximum expected consumer parallelism — the number of partitions is a hard ceiling on how many consumer instances within one group can do work in parallel. Over‑provisioning slightly is usually safer than under‑provisioning, since increasing partitions later can disrupt existing key‑based ordering guarantees.
Is SNS a queue or a topic?
SNS is a topic — a pure pub‑sub fan‑out service. It’s commonly paired with SQS in the “fan‑out” pattern: SNS broadcasts one message to several SQS queues, each of which then load‑balances its copy of the message across its own worker pool, combining both patterns in a single pipeline.
Summary
A message queue and a topic both decouple producers from consumers, but they answer fundamentally different fan‑out questions. A queue guarantees that each message is handled by exactly one consumer, making it the natural fit for distributing units of work across a scalable pool of workers. A topic delivers a copy of each message to every subscriber, making it the natural fit for broadcasting domain events to an arbitrary, evolving set of independent services. Modern log‑based systems like Kafka blur the line by letting you configure topics to behave like either, depending on consumer group setup — but the underlying conceptual distinction between “one consumer per message” and “every subscriber gets a copy” remains the single most important thing to understand before choosing.
In practice, the most resilient distributed systems don’t pick one pattern and force every interaction through it — they use topics to describe what happened, as durable, replayable, broadcastable facts that any current or future team can subscribe to, and they use queues to describe what needs to happen next, as discrete units of work that should be done exactly once by whichever worker is free. Learning to tell these two questions apart, message by message, is a skill that pays off long after any specific broker technology has been swapped out for the next one — RabbitMQ, Kafka, SQS, and SNS will all eventually be replaced by whatever comes next, but the underlying pattern of “one consumer per message” versus “every subscriber gets a copy” will still be the right first question to ask.
Key Takeaways
- Queue = one message, one consumer, work distributed across a pool.
- Topic = one message, every subscriber gets a copy, facts broadcast to many.
- Kafka topics can behave like queues when all consumers share one consumer group.
- Ordering is only guaranteed within a queue’s FIFO setting or a topic’s single partition — never globally across partitions.
- Both patterns are typically at‑least‑once by default — design consumers to be idempotent.
- Real production systems use both together: topics for domain events, queues for task distribution.
- Monitor queue depth and consumer lag as first‑class reliability signals, not afterthoughts.
- Secure messaging infrastructure like any other critical data path — authentication, encryption, and ACLs are not optional in production.