Home/Learn/System Design/Message Queues
system designadvanced

Message Queues Explained

Message queues decouple services by enabling asynchronous communication. A producer sends a message to a queue, and a consumer processes it later. This is fundamental to building scalable, resilient distributed systems.

1. Why Message Queues?

graph LR
    P["Producer"]
    Q["Message Queue"]
    C1["Consumer 1
(Payment)"]
    C2["Consumer 2
(Inventory)"]
    C3["Consumer 3
(Email)"]

    P -->|"Order created"| Q
    Q --> C1
    Q --> C2
    Q --> C3

    style P fill:#D97A2B,stroke:#B86418,color:#fff
    style Q fill:#FAF6EE,stroke:#E8DFC8
    style C1 fill:#FAF6EE,stroke:#E8DFC8
    style C2 fill:#FAF6EE,stroke:#E8DFC8
    style C3 fill:#FAF6EE,stroke:#E8DFC8
  • Decoupling: Services don't need to know about each other. Add new consumers without changing the producer.
  • Buffering: If the producer generates messages faster than the consumer processes them, the queue absorbs the burst.
  • Async processing: Heavy tasks (image processing, email sending) can be offloaded to background workers without blocking the main request.
  • Fan-out: One message can be consumed by multiple services simultaneously (pub/sub pattern).

2. Order Processing Example

sequenceDiagram
    participant Client as Client
    participant OrderSvc as Order Service
    participant Q as Kafka Queue
    participant PaymentSvc as Payment Service
    participant InventorySvc as Inventory Service
    participant EmailSvc as Email Service

    Client->>OrderSvc: Place order
    OrderSvc->>Q: Publish order-created event
    OrderSvc-->>Client: Order confirmed (async)
    Q->>PaymentSvc: Process payment
    Q->>InventorySvc: Reserve inventory
    Q->>EmailSvc: Send confirmation email
    Note over PaymentSvc,EmailSvc: All three consume independently

3. Kafka vs RabbitMQ

graph TD
    subgraph "Kafka"
        K["Event streaming platform
Messages stored in topics
Partitioned for parallelism
Retained after consumption"]
    end
    subgraph "RabbitMQ"
        R["Traditional message broker
Messages consumed and deleted
Complex routing (exchanges)
Message priority support"]
    end

    style K fill:#FAF6EE,stroke:#D97A2B
    style R fill:#FAF6EE,stroke:#D97A2B

Apache Kafka

An event streaming platform. Messages are stored in topics, partitioned for parallelism. Consumers read at their own pace. Messages are retained for a configurable time (even after consumption).

Best for: Event sourcing, log aggregation, real-time analytics, high-throughput pipelines.

RabbitMQ

A traditional message broker. Messages are consumed and deleted. Supports complex routing (topic exchanges, headers). Best for: task queues, RPC patterns, scenarios requiring message priority or TTL.

4. Delivery Guarantees

graph TD
    AtMost["At-most-once
Send once, never retry
Fast but may lose messages
Use: logging, metrics"]
    AtLeast["At-least-once
Retry until ACKed
No data loss, may duplicate
Consumer must be idempotent
Use: most workloads"]
    Exactly["Exactly-once
Delivered once
Extremely hard to achieve
Kafka: within single session
Use: financial transactions"]

    style AtMost fill:#FFF3CD,stroke:#FFC107
    style AtLeast fill:#D4EDDA,stroke:#28A745
    style Exactly fill:#D97A2B,stroke:#B86418,color:#fff
  • At-most-once: Message is sent once and never retried. Fast but may lose messages. Use when occasional data loss is acceptable.
  • At-least-once: Message is retried until acknowledged. No data loss but may cause duplicates. Consumer must be idempotent. Most common in practice.
  • Exactly-once: Message is delivered exactly once. Extremely hard to achieve. Kafka provides this within a single session using transactional APIs.

5. Message Ordering

Kafka guarantees ordering within a partition, not across partitions. If you need messages for a specific user to be processed in order, all messages for that user must go to the same partition. Use the user ID as the partition key.

// Partition key ensures ordering per user
producer.send({
  topic: "order-events",
  key: userId,    // Same userId → same partition → ordered
  value: { orderId, action: "created" }
});

6. Dead-Letter Queues

A dead-letter queue (DLQ) catches messages that fail processing after max retries. Without a DLQ, a poison pill message blocks the entire queue.

graph LR
    Q["Main Queue"]
    Consumer["Consumer"]
    DLQ["Dead-Letter Queue
(failed messages)"]
    Alert["Alert Team"]

    Q --> Consumer
    Consumer -->|"Max retries exceeded"| DLQ
    DLQ --> Alert

    style Q fill:#FAF6EE,stroke:#E8DFC8
    style Consumer fill:#D97A2B,stroke:#B86418,color:#fff
    style DLQ fill:#F8D7DA,stroke:#DC3545
    style Alert fill:#FFF3CD,stroke:#FFC107

7. Common Interview Mistakes

  • Not handling message retries: When a consumer fails, the message must be requeued with a retry count and dead-letter queue.
  • Ignoring poison pills: A malformed message that crashes the consumer blocks the entire queue. Use dead-letter queues.
  • Choosing Kafka for simple task queues: Kafka is overkill for basic task distribution. RabbitMQ or SQS is simpler.
  • Not partitioning correctly: Uneven partition distribution creates hot spots where one consumer handles most of the load.
  • Forgetting about monitoring: Queue depth, consumer lag, and processing latency must be tracked.

8. Summary

FeatureKafkaRabbitMQ
ModelEvent streamingMessage broker
RetentionConfigurable (days/weeks)Until consumed
OrderingWithin partitionPer queue
ThroughputMillions/secTens of thousands/sec
Use caseLog aggregation, analytics, event sourcingTask queues, RPC, notifications

Put it into practice

Ready to practice?

Start a mock interview with AI interviewer Alex. Get instant hiring signal.

Start a Mock Interview →