Message Queues - Complete Deep Dive

Prerequisites: Scalability, Microservices vs Monolith Used in: Chat System, Notification System, Digital Wallet, Zomato, Stock Broker


What is a Message Queue?

A message queue is a buffer that sits between a producer (sender) and a consumer (receiver). The producer puts messages in the queue. The consumer reads them at its own pace.

Real-world analogy: A mailbox. You (producer) drop a letter in. The recipient (consumer) picks it up whenever they’re free. You don’t need to wait for them to be home. The mailbox (queue) decouples the sender from the receiver.

Without queue (synchronous):
  Service A calls Service B directly
  If B is slow → A waits (blocks)
  If B is down → A fails

With queue (asynchronous):
  Service A → puts message in Queue → returns immediately
  Service B → reads from Queue when ready → processes
  If B is slow → messages queue up, A is unaffected
  If B is down → messages wait in queue until B recovers

Why Use Message Queues?

Benefit Example
Decoupling Payment Service doesn’t need to know about Email Service, SMS Service, Push Service
Async processing User uploads video → response immediately → encoding happens in background
Load leveling Burst of 10K orders → queue absorbs → workers process at steady 1K/sec
Reliability If consumer crashes, message stays in queue. No data loss.
Fan-out One event → consumed by multiple services independently

Messaging Patterns

1. Point-to-Point (Queue)

One message is consumed by exactly ONE consumer. Once processed, message is removed.

Producer → [Queue] → Consumer A (gets message 1)
                   → Consumer B (gets message 2)
                   → Consumer C (gets message 3)

Each message processed exactly once by one consumer.

Use case: Task processing, job scheduling, order processing Example: SQS, RabbitMQ (default mode)

2. Pub-Sub (Topic)

One message is delivered to ALL subscribers. Each subscriber gets a copy.

Publisher → [Topic] → Subscriber A (gets ALL messages)
                    → Subscriber B (gets ALL messages)
                    → Subscriber C (gets ALL messages)

Use case: Event broadcasting (order placed → notify inventory, analytics, email, all at once) Example: Kafka topics, SNS, Redis Pub/Sub

3. Consumer Groups (Best of Both)

Messages are distributed among consumers within a group (point-to-point WITHIN a group), but multiple groups each get all messages (pub-sub ACROSS groups).

Topic: "order_events"
  → Consumer Group "notification-service": messages split among 3 workers
  → Consumer Group "analytics-service": same messages split among 2 workers
  → Consumer Group "inventory-service": single worker gets all

Each group sees ALL messages, but within a group, each message goes to one worker.

This is how Kafka works. It’s the most common pattern in system design interviews.


Delivery Guarantees

Guarantee Meaning Trade-off
At-most-once Message might be lost, never delivered twice Fastest, lossy
At-least-once Message is guaranteed delivered, but might be duplicated Most common (SQS, Kafka default)
Exactly-once Message delivered exactly once Hardest to achieve, expensive

At-least-once is the standard for most systems. You handle duplicates on the consumer side using idempotency.

Producer sends message → Broker acknowledges
Consumer reads message → processes → acknowledges (commits offset)

If consumer crashes AFTER processing but BEFORE acknowledging:
  → Broker re-delivers the message
  → Consumer sees it again (duplicate!)
  → Solution: check idempotency key before processing

Message Ordering

Kafka: Messages within a PARTITION are ordered. Across partitions, no ordering guarantee.

Topic "orders" with 3 partitions:
  Partition 0: [order_1, order_4, order_7]  ← ordered within partition
  Partition 1: [order_2, order_5, order_8]  ← ordered within partition
  Partition 2: [order_3, order_6, order_9]  ← ordered within partition
  
But order_2 might be processed before order_1 (different partitions)

How to guarantee order for a specific entity: Use that entity’s ID as the partition key.

All messages with userId="user_123" → hash("user_123") → always goes to Partition 1
→ All events for user_123 are ordered

SQS Standard: No ordering guarantee. SQS FIFO: Ordered within a message group (similar to Kafka partitions).


Kafka vs SQS vs RabbitMQ

Feature Kafka SQS RabbitMQ
Model Log-based (append-only) Queue (delete after read) Queue + Exchange routing
Ordering Within partition No (Standard) / Yes (FIFO) Within queue
Retention Configurable (days/weeks/forever) 4 days (max 14) Until consumed
Replay Yes (re-read old messages) No (once read, gone) No
Throughput 1M+ messages/sec ~3000 msg/sec per queue ~20K msg/sec
Scaling Add partitions Unlimited (managed) Add queues/nodes
Consumer groups Built-in Not native Not native (use exchanges)
Managed option Confluent, MSK AWS SQS (fully managed) CloudAMQP, Amazon MQ
Best for Event streaming, CDC, high throughput Simple async tasks, decoupling Complex routing, RPC

When to Use Which

Choose Kafka when:

Choose SQS when:

Choose RabbitMQ when:


Key Concepts

Partitions (Kafka)

A topic is split into partitions. Each partition is an ordered, immutable log.

Topic: "payment_events" (3 partitions)

Partition 0: [msg_0, msg_3, msg_6, msg_9, ...]
Partition 1: [msg_1, msg_4, msg_7, msg_10, ...]
Partition 2: [msg_2, msg_5, msg_8, msg_11, ...]

Each partition is consumed by ONE consumer in a consumer group.
More partitions = more parallelism.

Rule of thumb: Number of partitions >= number of consumers in a group. If you have 10 consumers, you need at least 10 partitions.

Offset (Kafka)

Each message in a partition has a sequential offset (position number). Consumer tracks which offset it has processed.

Partition 0: [offset_0, offset_1, offset_2, offset_3, offset_4]
                                              ^
                                    consumer has processed up to here
                                    (committed offset = 3)

If consumer restarts, it resumes from committed offset (no duplicate processing if committed correctly).

Visibility Timeout (SQS)

When a consumer reads a message, it becomes “invisible” to other consumers for a timeout period. If consumer doesn’t delete it within that time, it becomes visible again (re-delivered).

Message in queue → Consumer A reads it → message invisible for 30s
  - Consumer A processes successfully → DELETE message → done
  - Consumer A crashes → 30s timeout → message becomes visible → Consumer B picks it up

Dead Letter Queue (DLQ)

Messages that fail processing N times get moved to a separate queue for investigation.

Main Queue → Consumer fails → retry 1 → fail → retry 2 → fail → retry 3 → fail
  ↓
Dead Letter Queue → message sits here → alarm fires → engineer investigates

Back-Pressure

Problem: Producer generates messages faster than consumer can process. Queue grows unbounded → runs out of memory/disk.

Solutions:

Strategy How
Bounded queue + reject Queue has max size. When full, producer gets error (back-pressure signal)
Rate limit producers Producer can only publish N messages/sec
Auto-scale consumers Add more consumer instances when queue depth grows
Alert on queue depth Alarm when queue > threshold → manual intervention

Common Interview Patterns

Pattern 1: Async Processing

User action → API responds immediately → Queue → Worker processes in background

Example: Video upload → “Upload successful” → Queue → Encoding worker → S3

Pattern 2: Event Fan-out

Order placed → Kafka topic "order_events"
  → Notification Service (sends email)
  → Inventory Service (reduces stock)
  → Analytics Service (tracks metrics)
  → Fraud Service (checks patterns)

Pattern 3: Work Distribution

10,000 emails to send → SQS queue → 20 Lambda workers → processed in parallel

Pattern 4: Rate Smoothing

Flash sale: 100K requests/sec spike → Queue buffers → Workers process at steady 10K/sec
(Queue absorbs the spike, prevents DB from dying)

Common Interview Questions

Q: “Why not just call the service directly?” A: Direct calls create tight coupling, no retry on failure, and blocking. With a queue: services are decoupled, messages survive failures, and consumers process at their own pace.

Q: “How do you handle duplicate messages?” A: At-least-once delivery means duplicates will happen. Handle on consumer side with idempotency keys — check if this messageId was already processed before executing.

Q: “How do you ensure ordering?” A: Kafka: use entity ID as partition key (all events for same entity go to same partition = ordered). SQS FIFO: use message group ID.

Q: “What if the queue goes down?” A: Managed queues (SQS, Confluent Kafka) are multi-AZ replicated. If self-hosted: Kafka replicates across brokers (replication factor=3 means 3 copies). Single point of failure is rare.

Q: “When would you NOT use a queue?” A: When you need a synchronous response (user is waiting for the result). When latency must be < 100ms. When the operation is idempotent and can be retried at the caller level without a queue.


← Back to Fundamentals ← Load Balancing Next: Database Concepts →

💬 Comments