Communication & Reliability· 20 min

Message Queue

Async communication between services (Kafka)

CommunicationIntermediate20 min read

Asynchronous communication between services — producers send messages, consumers process them later.

Message Queue — diagram
🖱️ Drag nodes · Scroll to zoom · Drag background to pan
publishpublishpublishconsumeconsumeconsumeOrder SvcPayment SvcNotify SvcMessage QueueKafka · 12 partitionsEmail Worker×3 instancesAnalytics×5 instancesAudit Logger×2 instances

High-level design

Producer → [Queue] → Consumer. Decouples producers from consumers. Enables async processing, retry on failure, load leveling.

Low-level design

Patterns: Point-to-point (one consumer per message), Pub/Sub (all subscribers get it). Delivery: at-most-once, at-least-once (consumers must be idempotent), exactly-once (hard). Ordering: FIFO preserves order but limits parallelism; standard parallelizes but may reorder.

Trade-offs

  • →At-least-once is simple but requires idempotent consumers
  • →FIFO guarantees order but caps throughput
  • →Queues add latency but improve resilience

Used in production by

RabbitMQ, Apache Kafka, AWS SQS/SNS, Redis Streams

Questions to ask your interviewer

  • →Can consumers handle duplicate messages?
  • →Is message ordering required?
  • →What happens if the queue is full?

System design deep dive

Message Queue: deep dive

A message queue lets one service hand off work to another without waiting for it, so spikes are absorbed, slow tasks move off the request path, and a failing consumer does not take the producer down with it. Interviews focus on delivery guarantees, ordering, and what happens when things go wrong.

How a message flows through a queue, step by step

  1. A user uploads a video. The API stores the raw file in object storage and publishes a small message {videoId: 7731, action: transcode} to the queue, then immediately returns 202 Accepted.
  2. The broker persists the message to disk (and to replicas, in Kafka or a replicated RabbitMQ quorum queue) before acknowledging the publish to the producer.
  3. A transcoding worker pulls the message. In SQS it becomes invisible to other consumers for a visibility timeout, for example 5 minutes; in Kafka the consumer simply reads from its assigned partition at its current offset.
  4. The worker processes the job and then acknowledges it (SQS delete, RabbitMQ ack, or a Kafka offset commit).
  5. If the worker crashes before acknowledging, the message becomes visible again or the offset is re-read, and another worker retries it. This is why delivery is at-least-once.
  6. After a configured number of failed attempts, say 5, the message moves to a dead-letter queue for inspection instead of blocking the pipeline forever.

Queue vs log: RabbitMQ/SQS vs Kafka

AspectTraditional queue (SQS, RabbitMQ)Distributed log (Kafka, Pulsar)
Message lifetimeDeleted once acknowledgedRetained for days regardless of consumption
Multiple consumersCompeting consumers share the workEach consumer group reads the full stream independently
ReplayNot possible after ackRewind the offset and reprocess history
OrderingBest effort (FIFO queues optional)Strict within a partition
ThroughputThousands to tens of thousands msg/s per queueMillions msg/s across a cluster
Typical useBackground jobs, task distributionEvent streaming, CDC, analytics pipelines

When to use a queue (and when not to)

Use a queue when the caller does not need the result right away: sending emails and push notifications, generating thumbnails, updating search indexes, fan-out of events to many services, or smoothing a traffic burst so a database receives a steady 2,000 writes per second instead of a 20,000 per second spike. Avoid a queue when the user is waiting on the answer (checking a password, reading a balance); a synchronous call is simpler and a queue only adds latency and failure modes. Also avoid using a queue as a database: it is a transport, and long-term state belongs in a store you can query.

Making consumers idempotent

Because redelivery is normal, every consumer should produce the same result if it sees the same message twice. The usual technique is a deduplication record keyed by a message ID, written in the same transaction as the side effect.

Idempotent consumer using a processed-messages table
BEGIN;
-- Fails with a unique violation if this message was already handled.
INSERT INTO processed_messages (message_id, processed_at)
VALUES ('evt_5f2a91', now());

UPDATE wallets SET balance = balance + 500 WHERE user_id = 42;
COMMIT;
-- On unique violation: roll back and ack the message; it is a duplicate.

Common mistakes in interviews

  • Claiming exactly-once delivery without explaining how. Real systems get effectively-once by combining at-least-once delivery with idempotent consumers or transactional writes.
  • Writing to the database and then publishing to the queue as two separate steps. If the process dies in between, the event is lost. Use the transactional outbox pattern.
  • Requiring global ordering, which forces a single partition and kills throughput. Usually ordering per entity (per order ID, per user) is enough, achieved by using that ID as the partition key.
  • No dead-letter queue, so one poison message blocks or endlessly retries.
  • Not monitoring consumer lag, the clearest signal that consumers are falling behind.

Interview questions

›What is the transactional outbox pattern?

Instead of publishing directly, the service writes the event into an outbox table in the same database transaction as the business change. A separate relay process (or change data capture tool such as Debezium) reads the outbox and publishes to the broker. The event is published if and only if the transaction committed.

›How does Kafka scale consumers?

A topic is split into partitions, and within a consumer group each partition is assigned to exactly one consumer. Throughput scales by adding partitions and consumers, but you cannot usefully have more consumers in a group than partitions, so partition count is chosen with future scale in mind.

›What is backpressure and how do queues provide it?

Backpressure is a way for a slow downstream to avoid being overwhelmed by a fast upstream. A queue buffers the excess so consumers process at their own pace, and the growing queue depth signals that you should add consumers. If the buffer can fill up, you also need a policy: block producers, drop low-priority messages, or reject with an error.

›How do you retry failed messages without hammering a broken dependency?

Use exponential backoff with jitter between attempts, for example 1s, 4s, 16s, then park the message in a dead-letter queue. Some systems use separate delay queues per retry tier. Pair this with a circuit breaker so consumers pause when the dependency is clearly down.

›When would you choose pub/sub over a point-to-point queue?

Choose pub/sub when several independent services need to react to the same event, such as an OrderPlaced event consumed by billing, shipping, and analytics. Point-to-point fits when a message represents one unit of work that exactly one worker should perform.

More System Design Concepts

Compare this lesson with the other 13 concepts in the course. Expand any card for its design, trade-offs and diagram.

Category:
Difficulty:
Showing 13 of 13