Message Queue Types: Point-to-Point vs Publish-Subscribe
Understand the two fundamental messaging patterns - point-to-point and publish-subscribe - and when to use each, including JMS, AMQP, and MQTT protocols.
Message queues are the backbone of asynchronous communication in distributed systems. They let services produce and consume messages without tight coupling or direct dependencies. Choosing the right queue type — point-to-point or pub/sub — shapes how your system handles failures, ordering, and scalability. The Publish/Subscribe Patterns post covers the pub/sub model in depth, while this guide focuses on the fundamentals both patterns share.
Introduction
Message queues are the backbone of asynchronous communication in distributed systems. They enable services to communicate without waiting for immediate responses, providing decoupling, reliability, and scalability.
This post covers the two fundamental messaging patterns — point-to-point and publish-subscribe — their delivery guarantees, operational considerations, and how to choose the right approach for your use case.
For implementations, see our posts on RabbitMQ, Apache Kafka, and AWS SQS/SNS.
Core Concepts
In point-to-point (P2P) messaging, each message goes to exactly one consumer. The queue holds messages until a consumer picks them up, then removes the message. If no consumer is available, the message waits.
graph LR
Producer[Producer] -->|message| Q[Queue]
Q -->|message 1| Consumer1[Consumer 1]
Q -->|message 2| Consumer2[Consumer 2]
Q -->|message 3| Consumer3[Consumer 3]
This pattern is useful for task distribution. Think of a queue of print jobs: each job goes to one printer, not all printers. The sender does not care which consumer handles it, only that someone does.
Point-to-Point
Point-to-Point Key Characteristics
- Each message goes to exactly one consumer
- Messages wait in the queue until a consumer picks them up
- Producers can outpace consumers; the queue absorbs the difference
- No fan-out—messages cannot automatically go to multiple consumers
Point-to-Point Common Use Cases
- Task processing like image resizing or video transcoding
- Background job queues
- Decoupling requesters from responders
- Load balancing across workers
Publish-Subscribe Messaging
Publish-subscribe (pub/sub) is a different model. Messages are published to a topic, and all subscribers to that topic receive a copy.
graph LR
Producer[Publisher] -->|message| Topic[Topic]
Topic -->|message| Consumer1[Subscriber 1]
Topic -->|message| Consumer2[Subscriber 2]
Topic -->|message| Consumer3[Subscriber 3]
The publisher has no idea who is listening. Subscribers opt into topics, and every matching message goes to all of them.
Topic Hierarchies
Many pub/sub systems let you structure topics hierarchically:
orders/
orders/created
orders/updated
orders/cancelled
orders/fulfilled
A subscriber to orders gets all order events. A subscriber to orders/created gets only creation events.
Pub/Sub
Pub/Sub Key Characteristics
- One-to-many delivery: each message goes to all subscribers
- Topic-based filtering: subscribers choose what to receive
- No built-in message persistence: most systems don’t store messages for offline subscribers
- Fan-out: the same message reaches multiple consumers
Pub/Sub Common Use Cases
- Broadcasting events like user signups or order placements
- System-wide notifications
- Replicating data across services
- Pushing real-time updates to multiple clients
Comparing the Patterns
| Aspect | Point-to-Point | Publish-Subscribe |
|---|---|---|
| Delivery | One consumer per message | All subscribers per message |
| Coupling | Producer to queue to consumer | Publisher to topic to subscribers |
| Data flow | Single consumer | Fan-out to all subscribers |
| State | Queue holds messages | Topics typically transient |
| Use case | Task distribution | Event broadcasting |
Delivery Guarantees
Message queues offer different delivery guarantees. The semantics you pick directly affect how your consumers handle failures and duplicates.
Delivery Guarantees Overview
At-Most-Once Delivery (QoS 0)
The broker fires the message at the consumer and does not wait for acknowledgment. If the consumer crashes before processing, the message is gone. This is the “fire and forget” model.
Use when: You can afford to lose messages occasionally. Sensor data where freshness matters more than completeness fits here.
At-Least-Once Delivery (QoS 1)
The broker holds the message until the consumer acknowledges it. If the consumer times out or crashes, the message gets redelivered. You may see duplicates.
Use when: Missing messages is worse than processing duplicates. Billing reconciliation, order processing—anywhere you need confirmation that the message was handled.
Exactly-Once Delivery (QoS 2)
A two-phase protocol prevents both loss and duplicates. The broker and consumer negotiate delivery in two hops: first a prepare, then a commit. This costs throughput but eliminates duplicate processing.
Use when: Financial transactions where duplicates have real consequences. The performance hit is substantial, so most systems settle for at-least-once with idempotent consumers.
graph LR
subgraph "At-Most-Once"
A1[Broker sends] --> A2[Consumer receives]
A2 -.-> A3[Crash = message lost]
end
subgraph "At-Least-Once"
B1[Broker sends] --> B2[Consumer ACKs]
B2 -.-> B3[Timeout = redeliver]
end
subgraph "Exactly-Once"
C1[Prepare] --> C2[Commit]
C2 --> C3[No loss, no duplicate]
end
Idempotent Consumers
To get exactly-once behavior without QoS 2 overhead, make your message processing idempotent. Use a unique message ID as a deduplication key. Store processed IDs in Redis or a database with a short TTL. If you see the same ID twice, skip processing.
// Idempotent message processor
public void processMessage(Message msg) {
String msgId = msg.getMessageId();
if (processedIds.contains(msgId)) {
return; // Already handled, skip
}
// ... do the actual work ...
processedIds.add(msgId);
}
Fault Tolerance
Messages that fail repeatedly need somewhere to go. Dead letter queues (DLQs) catch them so they do not block the main queue.
Fault Tolerance Overview
How DLQs Work
When a message exceeds your retry limit or fails with a permanent exception, the broker moves it to a DLQ instead of discarding it. The DLQ holds the original message plus metadata about the failure: exception type, error message, retry count, timestamps.
Configuring Dead Letter Queues
Most brokers let you configure DLQ behavior per queue:
// ActiveMQ Artemis DLQ configuration
QueueConfiguration config = new QueueConfiguration()
.setName("orders.queue")
.setDeadLetterAddress("orders.dlq")
.setMaxDeliveryAttempts(5)
.setDeadLetterQueueDelay(60000); // Wait 1 min before moving to DLQ
artemis.createQueue(config);
# RabbitMQ dead letter exchange setup
channel.exchange_declare(exchange='orders.dlx', exchange_type='direct')
channel.queue_declare(queue='orders.dlq')
channel.queue_bind(queue='orders.dlq', exchange='orders.dlx', routing_key='dead')
# Main queue with DLX
channel.queue_declare(
queue='orders',
arguments={
'x-dead-letter-exchange': 'orders.dlx',
'x-dead-letter-routing-key': 'dead',
'x-message-ttl': 86400000 # 24 hour TTL
}
)
DLQ Monitoring and Processing
DLQs need active monitoring. Set up alerts:
- Alert when DLQ depth exceeds zero
- Log DLQ arrivals with failure context
- Have a process to investigate and retry or discard DLQ messages
- Consider a separate DLQ consumer that can route messages back to the main queue after fixes
Poison Message Handling
Some messages keep failing no matter what. These “poison messages” can lock up a queue if they always get redelivered. Set maxDeliveryAttempts to put a hard limit on retries. After that, the DLQ takes over.
Flow Control
When producers outpace consumers, you need strategies to handle overflow.
Flow Control Overview
Prefetch Limits
Brokers like RabbitMQ let you limit how many messages a consumer has “in flight” at once. Set prefetch=10 and the broker stops sending after 10 unacknowledged messages. The consumer catches up before getting more. This prevents memory exhaustion on slow consumers.
// RabbitMQ prefetch configuration
channel.basicQos(10); // Max 10 unacked messages
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
// Process message
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
Flow Control in Kafka
Kafka consumers control their own read pace by committing offsets. If a consumer falls behind, it just means lag—more data sitting on disk waiting to be read. Monitor consumer lag as a key metric. If lag grows faster than you can catch up, add consumer instances to the group.
# Check consumer lag
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group my-consumer-group --describe
# Output shows current lag per partition
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# orders 0 5000 5500 500
Message Throttling
Some systems let you slow down producers when the queue fills. RabbitMQ has per-connection credit flow—producers pause when the broker runs low on resources. This is gentler than hard rejections when the queue is full.
Consumer Scaling
You can add more consumers to catch up. For Kafka, add more partitions and spread the load. For queue-based systems, add more worker instances. Just make sure your consumers are stateless so they can run in parallel.
Circuit Breakers
When consumers start failing (database down, API timeout), do not keep hammering them with messages. Implement circuit breakers that pause consumption when error rates spike. The queue holds messages while you recover.
Ordering Guarantees
Some workloads need messages processed in order. Here is what different brokers actually guarantee.
Ordering Guarantees Overview
FIFO Queues
True FIFO requires a single consumer per queue. With one consumer, messages leave in the same order they arrived. This is the simplest case, but gives up parallelism.
Partitioned Topics
Kafka provides ordering within a partition. If you partition by orderId, all events for the same order go to the same partition, and that partition is processed by exactly one consumer. You get ordering plus parallelism.
Topic: orders
Partitions: 3 (partitioned by orderId % 3)
order-101 → partition 1
order-102 → partition 2
order-103 → partition 3
order-104 → partition 1 (same partition as 101, processed in order)
Sequence Numbers
For systems that need global ordering across multiple consumers, embed a sequence number in each message. The consumer tracks the highest sequence number it has seen and rejects any message with a lower number.
public void processMessage(Message msg) {
long seq = msg.getSequenceNumber();
if (seq <= lastProcessedSeq) {
return; // Out of order, skip
}
lastProcessedSeq = seq;
// Process the message
}
Pattern Comparison
| Pattern | Ordering | Parallelism | Complexity |
|---|---|---|---|
| Single consumer queue | Full FIFO | None | Low |
| Partitioned topic (1 partition) | FIFO within partition | Low | Medium |
| Partitioned topic (N partitions) | FIFO per partition | N consumers | Medium |
| Sequence numbers | Global ordering | Any | High |
Trade-off Analysis
| Dimension | Point-to-Point | Publish-Subscribe |
|---|---|---|
| Delivery | Exactly one consumer | All subscribers receive copy |
| Scalability | Load leveling across workers | Fan-out to many subscribers |
| Fault Tolerance | Queue absorbs producer spikes | Subscribers must stay online |
| Ordering | FIFO with single consumer | No ordering guarantee |
| Complexity | Simple queue setup | Topic subscription management |
| Use Case | Task distribution, work queues | Event broadcasting, notifications |
| Backpressure | Prefetch limits + DLQs | Slow subscribers lose messages |
| Monitoring | Queue depth + consumer lag | Subscriber count + topic metrics |
Protocol Comparison
Here is how the major messaging protocols stack up:
| Feature | AMQP 1.0 | AMQP 0-9-1 | MQTT | CoAP |
|---|---|---|---|---|
| Model | Point-to-point + pub/sub | Point-to-point + pub/sub | Primarily pub/sub | Request/response |
| Wire protocol | Binary | Binary | Binary | Binary |
| Header overhead | ~40 bytes | ~40 bytes | ~2 bytes | ~4 bytes |
| Connection | Long-lived | Long-lived | Long-lived | Short-lived |
| QoS levels | 3 | 3 | 3 | 3 |
| Topics | Hierarchical | Hierarchical | Hierarchical | Observer pattern |
| Transactions | Supported | Not standard | Not standard | Not standard |
| Portable | Yes (standardized) | Vendor-specific | Limited | Limited |
| Typical use | Enterprise messaging | RabbitMQ classic | IoT/sensors | IoT constrained |
AMQP 1.0 is the most feature-complete and standardized, wire-compatible across implementations.
AMQP 0-9-1 (RabbitMQ classic) has richer features but locks you into a specific implementation.
MQTT targets low-bandwidth, unreliable networks. It is the de facto standard for IoT.
CoAP targets extremely constrained devices like 8-bit microcontrollers, using HTTP-like requests over UDP.
Message Broker Selection Flowchart
Given a new project, here is how to narrow down which broker fits:
graph TD
Start[New messaging project] --> Scale{What's your scale?}
Scale -->|Millions of msgs/day| HighVolume{High throughput?}
HighVolume -->|Yes| KafkaOrArtemis{Area of focus?}
HighVolume -->|No| MediumScale
Scale -->|Thousands of msgs/day| MediumScale[Standard relational DB<br/>or lightweight broker]
KafkaOrArtemis -->|Distributed streaming<br/>log, event sourcing| Kafka[Apache Kafka]
KafkaOrArtemis -->|Enterprise messaging<br/>transactions, AMQP| Artemis[ActiveMQ Artemis]
MediumScale --> NeedsMultiProtocol{Need multi-protocol?}
NeedsMultiProtocol -->|AMQP + MQTT + STOMP| Artemis
NeedsMultiProtocol -->|Just AMQP 0-9-1| RabbitMQ[RabbitMQ]
NeedsMultiProtocol -->|No| CloudManaged{AWS or cloud-native?}
CloudManaged -->|Yes| AWSSQS{AWS-based?}
CloudManaged -->|No| SelfHosted{Self-hosted OK?}
AWSSQS -->|FIFO / exactly-once| SQSFIFO[Amazon SQS FIFO]
AWSSQS -->|Pub/sub, fan-out| SNS[Amazon SNS]
SelfHosted -->|Yes| RabbitMQ
SelfHosted -->|No| AzureEvent{Using Azure?}
AzureEvent -->|Yes| AzureEventHubs[Azure Event Hubs]
AzureEvent -->|No| GCPubsub{GCP?}
GCPubsub -->|Yes| GCPPubSub[Google Cloud Pub/Sub]
GCPubsub -->|No| NATS[NATS]
Quick reference by constraint:
| Constraint | Best choice |
|---|---|
| Exactly-once across systems | SQS FIFO, Kafka (transactions) |
| Message persistence + disk-backed | Kafka (retention), Artemis (journal) |
| AMQP 1.0 native | ActiveMQ Artemis |
| Multi-protocol (AMQP + MQTT + STOMP) | ActiveMQ Artemis |
| Team familiar with RabbitMQ | RabbitMQ |
| Already on AWS | SQS + SNS |
| IoT / extremely lightweight | NATS, MQTT brokers |
| Event sourcing / immutable log | Apache Kafka |
| Highest throughput possible | Apache Kafka, ActiveMQ Artemis |
Pick point-to-point when:
- A message needs processing by exactly one consumer
- You need load leveling (producers faster than consumers)
- Tasks should be processed in order or with fairness
Pick publish-subscribe when:
- Multiple consumers need the same message
- You are broadcasting events to many services
- Consumers are independent and all need to react
Most real systems use both. Order events might go to a topic (for audit logs, notifications, and analytics), while specific order fulfillment tasks go to a queue for the fulfillment worker.
For deeper dives, see our posts on RabbitMQ, Apache Kafka, and AWS SQS/SNS.
Topic-Specific Deep Dives
JMS: The Java Standard
Java Message Service (JMS) is an API standard for messaging. It defines interfaces, not implementations. You write to the JMS API, and your underlying provider (ActiveMQ, RabbitMQ, IBM MQ) handles the details.
JMS supports both queues and topics:
// Point-to-point
Queue queue = session.createQueue("tasks");
MessageProducer producer = session.createProducer(queue);
producer.send(session.createTextMessage("process this"));
QueueReceiver receiver = session.createReceiver(queue);
Message msg = receiver.receive();
// Publish-subscribe
Topic topic = session.createTopic("events");
MessageProducer producer = session.createProducer(topic);
producer.send(session.createTextMessage("something happened"));
TopicSubscriber subscriber = session.createSubscriber(topic);
Message msg = subscriber.receive();
JMS 2.0 simplified the API and added delivery delays, but the core ideas did not change. The old API required verbose setup: factory, connection, start, session, then finally producer and consumer. JMS 2.0 cut through the boilerplate significantly.
JMS 1.x vs 2.0 API Comparison
// JMS 1.x — verbose setup for every message
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("tasks");
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage("process this");
producer.send(message);
session.close();
connection.close();
// JMS 2.0 — @Inject approach uses CDI (Contexts and Dependency Injection)
// Container manages connection, session lifecycle automatically
@Inject
private JMSContext context;
@Inject
private Queue tasksQueue;
public void sendTask(String taskData) {
// One-liner send — no manual session management
context.createProducer().send(tasksQueue, taskData);
}
// Receiving with simplified API
@Inject
private JMSContext context;
public String receiveTask() {
// receive() blocks; receiveBody() returns the typed payload directly
return context.createConsumer(tasksQueue).receiveBody(String.class);
}
The @Inject JMSContext pattern works well in Java EE / Jakarta EE environments. For Spring, JmsTemplate wraps the JMS API with similar convenience.
ActiveMQ Artemis: Modern AMQP
ActiveMQ Artemis is the actively developed successor to classic ActiveMQ. It speaks AMQP 1.0 natively, plus MQTT, STOMP, and HornetQ native protocols, with a different architecture under the ActiveMQ umbrella.
Artemis uses an address-based model rather than the older queue/topic split. Bind addresses to queues with different routing semantics, and you can build almost any pattern you need.
// ActiveMQ Artemis — address-based routing
// Messages sent to "orders.created" address
Address address = session.createAddress("orders.created");
// Queue bound to the address receives all messages
Queue queue = session.createQueue("orders-queue").bind(address);
// Fully Qualified Domain Naming for clustering
// artemis: clustering://artemis01.example.com,5672
Artemis vs RabbitMQ
| Aspect | ActiveMQ Artemis | RabbitMQ |
|---|---|---|
| AMQP version | AMQP 1.0 only | AMQP 0-9-1 (classic), 1.0 via plugin |
| Protocol support | AMQP, MQTT, STOMP, HornetQ, OpenWire | AMQP 0-9-1, MQTT, STOMP |
| Queue model | Address-based (any pattern) | Exchange + binding (classic) |
| Clustering | Master/slave, replicated (Janus) | Standard master/slave |
| Message count | Millions per second | ~50K-100K/second sustained |
| Disk persistence | Journal (append-only, very fast) | Mnesia + transient messages |
| Client languages | Any with AMQP 1.0 client | Erlang (OTP), many language clients |
Artemis has higher raw throughput and the address-based model gives more routing flexibility. RabbitMQ has operational maturity and a larger ecosystem to draw from.
CloudEvents: Vendor-Neutral Event Format
Most messaging systems define their own message envelope format. CloudEvents, a CNCF specification, standardizes how events look across different systems so you are not locked into one vendor’s schema.
{
"specversion": "1.0",
"id": "message-uuid-12345",
"source": "//my-service/orders",
"type": "com.example.order.created",
"subject": "order-789",
"time": "2026-03-26T10:15:30Z",
"datacontenttype": "application/json",
"data": {
"order_id": "789",
"customer_id": "cust-456",
"total": 129.99
},
"extensions": {
"traceparent": "00-abc123-def456-01"
}
}
The source field identifies where the event came from, type uses reverse-DNS naming to describe what happened, and subject pinpoints which entity the event is about. Extensions carry vendor-specific metadata like distributed trace context.
# Publishing a CloudEvent with cloudevents-sdk
from cloudevents.sdk import CloudEvent
# Create a CloudEvent
ce = CloudEvent(
source="//my-service/orders",
type="com.example.order.created",
data={"order_id": "789", "customer_id": "cust-456", "total": 129.99}
)
# Serialize to JSON (HTTP binding)
from cloudevents.sdk.http import to_http
headers, body = to_http(ce)
# headers['Content-Type'] = 'application/cloudevents+json'
Many platforms now produce or consume CloudEvents natively: AWS EventBridge, Azure Event Grid, Google Cloud Events, Knative, and Solace all support it. Using CloudEvents as your internal event format means you can plug into any of these platforms without rewriting event producers or consumers.
If JMS is an API, think of AMQP as a wire protocol—defining how clients and brokers talk over the network, making it language-agnostic.
AMQP’s model has three main pieces:
- Exchange: Takes messages from publishers and routes them based on rules
- Queue: Holds messages until consumers pick them up
- Binding: Tells an exchange which queue to route which messages to
graph LR
Publisher -->|publish| Exchange[Exchange]
Exchange -->|route| Q1[Queue 1]
Exchange -->|route| Q2[Queue 2]
Exchange -->|route| Q3[Queue 3]
The exchange type determines routing behavior:
- Direct: Route to queue matching the routing key exactly
- Fanout: Route to all bound queues
- Topic: Route to queues matching wildcard patterns
AMQP supports both P2P (via direct exchange to single queue) and pub/sub (via fanout or topic exchanges).
MQTT: Lightweight for IoT
MQTT was built for constrained devices and unreliable networks. It dominates IoT where bandwidth is scarce and devices go offline often.
MQTT vocabulary differs from the mainstream:
- Broker instead of server
- Client instead of consumer
- QoS levels instead of delivery guarantees
MQTT QoS levels:
- QoS 0: At most once—fire and forget, no acknowledgment
- QoS 1: At least once—message arrives, consumer acknowledges
- QoS 2: Exactly once—two-phase delivery prevents duplicates
The lightweight design makes MQTT suitable for sensors and actuators that cannot handle the overhead of AMQP or JMS.
When to Use / When Not to Use
When to Use Point-to-Point Messaging
- Task distribution: exactly one worker processes each task
- Load leveling: producers outpace consumers and you need a buffer
- Request/response decoupling: sender does not need an immediate reply
- Ordered processing: messages must be handled in sequence
When Not to Use Point-to-Point
- Fan-out: multiple consumers need the same message
- Simple notifications: broadcasting is the goal
- Event broadcasting: many services need to know about the same fact
When to Use Publish-Subscribe Messaging
- Event broadcasting: one event should trigger multiple independent actions
- System-wide notifications: several services need to react to the same fact
- Decoupled microservices: services should not need to know about each other
- Real-time updates: pushing updates to multiple clients or services
When Not to Use Publish-Subscribe
- Sequential processing: order matters and only one consumer should handle it
- Request/response: you need a reply from a specific service
- Task queues: work needs assignment to specific available workers
| Failure | Impact | Mitigation |
|---|---|---|
| Broker goes down | Messages cannot be sent or received | Cluster with replication; use durable queues |
| Consumer crash mid-processing | Message lost (auto-ack) or reprocessed | Manual acknowledgments; idempotent processing |
| Network partition | Messages stuck or delayed | Connection recovery; appropriate socket timeout |
| Queue overflow | New messages rejected or old dropped | Max queue length policies; monitor queue depth |
| Message TTL expiration | Unprocessed messages disappear | Appropriate TTL; dead letter queues for failures |
| Duplicate message delivery | Same message processed multiple times | Idempotent consumers; deduplication keys |
| Routing key mismatch | Messages go to wrong queue or nowhere | Consistent naming; dead letter exchanges |
Production Failure Scenarios
Common Pitfalls / Anti-Patterns
Pitfall 1: Treating Pub/Sub like a Queue
Pub/sub broadcasts to all subscribers. If only one should process a message, put a queue in front of the subscriber or implement filtering correctly.
Pitfall 2: Ignoring Message Ordering Requirements
If your business logic requires ordering, your architecture must deliver it. Point-to-point with a single consumer or partitioned topics in Kafka can provide that guarantee.
Pitfall 3: Auto-Acknowledgment Without Idempotency
Auto-ack discards messages immediately on delivery. If the consumer crashes, the message is gone. Pair auto-ack with idempotent processing, or just use manual acknowledgment instead.
Pitfall 4: Not Handling Poison Messages
Messages that keep failing block the queue and hold up everything behind them. Configure dead letter queues and set retry limits.
Pitfall 5: Coupling Publishers to Topic Structure
When publishers know too much about subscriber interests, changing subscribers means changing publishers. Keep topic design stable and use content-based filtering for flexibility.
Pitfall 6: Using a Single Queue for Multiple Concerns
Mixing message types in one queue makes processing complex and error-prone. Use separate queues or topics per message type.
Interview Questions
Expected answer points:
- Point-to-point: Each message goes to exactly one consumer; queue holds messages until processed
- Publish-subscribe: Messages published to a topic; all subscribers receive a copy
- P2P is for task distribution, pub/sub is for event broadcasting
- P2P provides load leveling; pub/sub provides fan-out to multiple consumers
Expected answer points:
- QoS 0 (At most once): Fire and forget, no acknowledgment, messages may be lost
- QoS 1 (At least once): Consumer acknowledges, messages may be duplicated but not lost
- QoS 2 (Exactly once): Two-phase delivery prevents both loss and duplicates
- QoS 0 for high-frequency, losable data like sensor readings; QoS 1 for general IoT; QoS 2 for critical commands
Expected answer points:
- A DLQ captures messages that fail repeatedly and cannot be processed
- Prevents poison messages from blocking the main queue indefinitely
- Stores original message plus failure metadata (exception type, retry count, timestamps)
- Enables monitoring and manual intervention for failed messages
- Configured via max delivery attempts and dead letter address settings
Expected answer points:
- Prefetch limits control how many messages a consumer has in-flight at once
- Flow control pauses producers when broker resources are low
- Consumer scaling adds more instances to process messages faster
- Circuit breakers pause consumption when error rates spike
- Kafka consumer lag indicates how far behind consumers are; scaling partitions helps
Expected answer points:
- JMS is a Java API specification; it defines interfaces not implementations
- AMQP is a wire protocol defining how clients and brokers communicate over the network
- JMS is Java-centric; AMQP is language-agnostic
- JMS providers (ActiveMQ, HornetQ) implement the API; AMQP clients work across implementations
- JMS 2.0 simplified the API with CDI injection; older 1.x required verbose boilerplate
Expected answer points:
- QoS 2 in MQTT provides exactly-once via two-phase protocol (prepare then commit)
- Idempotent consumers: store processed message IDs and skip duplicates
- Deduplication keys: use unique message IDs with short TTL in Redis or database
- Transactional outbox pattern: write to database and message queue atomically
- Trade-off: exactly-once is expensive; most systems use at-least-once with idempotent consumers
Expected answer points:
- Kafka provides ordering within a partition, not across the entire topic
- Partitioning by a key (e.g., orderId) ensures all events for the same entity go to the same partition
- Single-partition topics give you FIFO ordering but no parallelism
- Traditional queues with a single consumer also provide FIFO; parallelism requires careful design
- Consumer lag monitoring is critical for Kafka ordering guarantees
Expected answer points:
- Artemis speaks AMQP 1.0 natively; RabbitMQ uses AMQP 0-9-1 (classic) plus 1.0 via plugin
- Artemis has address-based routing; RabbitMQ uses exchange + binding model
- Artemis handles millions of messages per second; RabbitMQ sustains ~50K-100K/second
- Artemis uses append-only journal for persistence; RabbitMQ uses Mnesia
- Artemis supports MQTT, STOMP, HornetQ, OpenWire; RabbitMQ supports AMQP, MQTT, STOMP
Expected answer points:
- CloudEvents is a CNCF specification for vendor-neutral event format
- Standardizes event structure across different systems and cloud providers
- Includes specversion, id, source, type, subject, time, datacontenttype, and data fields
- Extensions field carries vendor-specific metadata like distributed trace context
- Supported natively by AWS EventBridge, Azure Event Grid, Google Cloud Events, Knative, Solace
Expected answer points:
- SQS for simple point-to-point queues with managed infrastructure; FIFO option for ordering
- SNS for pub/sub fan-out to multiple subscribers; pairs well with SQS for queue-backed subscribers
- Kafka for high-throughput event streaming, event sourcing, or immutable logs
- Consider: throughput needs, ordering requirements, protocol support, operational complexity
- Hybrid approach: SNS for pub/sub notifications, SQS for task queues, Kafka for event streaming
Expected answer points:
- Queue-based: messages go to one consumer that pulls from the queue; load balancing across consumers possible
- Topic-based: messages published to a topic broadcast to all subscribers; each gets a copy
- Queues use direct addressing (consumer pulls); topics use subscription addressing (broker pushes to subscribers)
- Topic models enable fan-out patterns; queue models enable work distribution patterns
- Most messaging systems support both models (RabbitMQ exchanges, JMS queues/topics, SQS/SNS)
Expected answer points:
- Consumers acknowledge messages after successful processing to tell the broker the message was handled
- Auto-ack: broker removes message immediately on delivery; risky if consumer crashes before processing
- Manual ack: consumer controls when to acknowledge; enables retry on failure, exactly-once semantics
- Acknowledgment latency affects throughput; batching acks can improve performance
- Without proper acknowledgments, messages can be lost (auto-ack + crash) or duplicated (no ack + redelivery)
Expected answer points:
- Problem: writing to a database and publishing a message atomically is hard without distributed transactions
- Solution: write both the business record AND an outbox record in the same database transaction
- A separate process polls the outbox table and publishes messages to the broker
- Guarantees at-least-once delivery since the outbox record persists the intent to publish
- Used in CDC (change data capture) and event sourcing patterns to avoid dual-writes
Expected answer points:
- Single consumer per queue: messages processed in FIFO order (simplest case)
- Partitioned topics (Kafka): messages with same partition key maintain ordering; different partitions can be processed in parallel
- Sequence numbers: embed incrementing sequence in each message; consumer tracks highest seen and skips out-of-order
- Sharding by entity: all events for the same entity (e.g., orderId) go to the same consumer or partition
- Trade-off: strict ordering reduces parallelism; most systems can tolerate eventual ordering with idempotency
Expected answer points:
- Synchronous: caller blocks until response; simple mental model, easy debugging, tight coupling
- Asynchronous: caller sends and forgets; better fault tolerance, backpressure handling, scalability
- Synchronous suffers from cascading failures; async decouples producer from consumer failures
- Async adds complexity: message ordering, delivery guarantees, idempotency, DLQ handling
- Hybrid: use async for business events, synchronous for user-facing requests that need immediate response
Expected answer points:
- Prefetch limits unacknowledged messages in-flight to a single consumer
- Low prefetch (e.g., 1): low memory usage, higher latency, better load balancing across slow consumers
- High prefetch (e.g., 100+): batch processing efficiency, higher throughput, more memory per consumer
- Prefetch 0: consumer receives all available messages at once (RabbitMQ behavior differs)
- Kafka uses `fetch.min.bytes` and `max.poll.records` instead of prefetch; controls how much data is returned per poll
Expected answer points:
- Persistence: messages are written to disk (or journal) before acknowledgment; survives broker restart
- Durability: queue/topic survives broker restart (depends on persistence + clustering + replication)
- Non-persistent messages may be lost on broker crash; persistent messages are written to disk
- Kafka: uses segment files and configurable `log.retention.hours`; messages persist until retention expires
- Trade-off: persistence adds latency (disk I/O); use for critical messages, disable for high-throughput low-value data
Expected answer points:
- Producer sends message with a `replyTo` header specifying a temporary response queue
- Consumer processes message and sends reply to the `replyTo` queue with correlation ID
- Producer consumes from response queue, matching `correlation_id` to original request
- TTL on response queue cleans up unanswered requests; timeout on producer side handles lost replies
- Pattern is async by nature; caller must handle waiting for response without blocking the thread
Expected answer points:
- Consumer group: set of consumers that share the work; each partition goes to exactly one consumer in the group
- If a consumer crashes, its partitions are rebalanced to other group members automatically
- Traditional queues: only one consumer receives each message (competing consumers pattern)
- Kafka: multiple consumer groups can each read the same messages independently (pub/sub semantics)
- Number of consumers in a group should not exceed number of partitions for maximum parallelism
Expected answer points:
- Use queue as a buffer between producers and consumers; queue absorbs the spike, consumers process at their pace
- Set appropriate queue capacity and overflow policies (reject new messages, overflow to disk, etc.)
- Implement consumer scaling: add more consumers or partitions to process backlog faster
- Use message throttling: slow down producers when queue depth exceeds threshold
- Monitor queue depth, consumer lag, and error rates; set up alerts for anomalies
- Design idempotent consumers so duplicate processing during catch-up is safe
Further Reading
- RabbitMQ - Deep dive into RabbitMQ exchanges, bindings, and practical patterns
- Apache Kafka - Event streaming, partitions, and consumer groups
- AWS SQS/SNS - Managed cloud messaging services
- Event-Driven Architecture - Patterns for building loosely coupled systems
Conclusion
Quick Recap Checklist
- Point-to-point (P2P): each message goes to exactly one consumer — use for task distribution, load leveling
- Publish-subscribe (pub/sub): each message goes to all subscribers — use for event broadcasting, fan-out
- At-least-once delivery with idempotent consumers is the practical choice for most systems
- Dead letter queues (DLQs) handle poison messages; configure retry limits
- Prefetch limits prevent slow consumers from being overwhelmed
- Circuit breakers pause consumption when error rates spike
- Message ordering requires single consumer (FIFO) or partitioned topics (Kafka)
- JMS is a Java API; AMQP is a wire protocol; MQTT is for constrained IoT devices
- CloudEvents provides vendor-neutral event format for cross-platform interoperability
- ActiveMQ Artemis: AMQP 1.0, millions/sec throughput, address-based routing
- RabbitMQ: AMQP 0-9-1, ~50-100K/sec, exchange+binding model, mature ecosystem
- Kafka: high-throughput event streaming, immutable log, consumer group parallelism
- SQS/SNS: managed cloud queues; FIFO for ordering, pub/sub via SNS fan-out
- Idempotent consumers prevent duplicate processing with at-least-once delivery
- Transactional outbox pattern avoids dual-write inconsistency in database+queue scenarios
Key Points
- Point-to-point delivers each message to exactly one consumer; publish-subscribe delivers to all subscribers
- P2P works well for task distribution and work queues; pub/sub works well for event broadcasting
- JMS is a Java API standard; AMQP is a wire protocol; MQTT is lightweight for IoT
- Queues give you persistence and load leveling; topics give you fan-out and flexibility
- Design for at-least-once delivery with idempotent consumers
Pre-Deployment Checklist
- [ ] Queue depth monitoring configured
- [ ] Dead letter queues configured for failed messages
- [ ] Manual acknowledgment implemented (preferred over auto-ack)
- [ ] Idempotent message processing implemented
- [ ] Retry limits set with exponential backoff
- [ ] TLS/encryption enabled for client connections
- [ ] Consumer group scaling strategy defined
- [ ] Message TTL configured appropriately
- [ ] Alert thresholds set for queue depth and error rates
- [ ] Schema validation in place for incoming messages
- [ ] Correlation ID propagation implemented for distributed tracing
Observability Checklist
Metrics to Monitor
- Queue depth: number of messages waiting to be processed
- Consumer lag: time between message publication and consumption
- Message throughput: messages published or consumed per second
- Error rate: failed message processing attempts
- Acknowledgment latency: time taken to acknowledge messages
- Connection count: active producers and consumers
Logs to Capture
- Message publish events with routing keys and timestamps
- Consumer acknowledgment and rejection events
- Dead letter queue arrivals with failure reasons
- Connection open and close events
- Retry attempts with attempt counts
Alerts to Configure
- Queue depth exceeds threshold, indicating burst traffic or consumer failure
- Consumer lag exceeds your SLA threshold
- High error rate on message processing
- Dead letter queue accumulating messages
- Broker connection failures
- Consumer disconnection events
Security Checklist
- Authentication: SASL or TLS client authentication for brokers
- Authorization: Queue or topic-level access controls; principle of least privilege
- Encryption in transit: TLS for all client connections
- Encryption at rest: disk encryption for message persistence
- Message validation: validate message schemas before processing
- Input sanitization: sanitize routing keys and message content to prevent injection
- Audit logging: log all administrative operations on queues and topics
- Network segmentation: place brokers in private networks; restrict access via firewalls
Category
Related Posts
Publish/Subscribe Patterns: Topics, Subscriptions, Filtering
Learn publish-subscribe messaging patterns: topic hierarchies, subscription management, message filtering, fan-out, and dead letter queues.
CQRS Pattern
Separate read and write models. Command vs query models, eventual consistency implications, event sourcing integration, and when CQRS makes sense.
Event Sourcing
Storing state changes as immutable events. Event store implementation, event replay, schema evolution, and comparison with traditional CRUD approaches.