InterviewKata
System design · Module 18 of 29 · Level 4: Distributed and async

Queues and event streams

Not every piece of work has to happen while the user waits. Put it on a queue and let another service do it when it can: the user gets a fast answer, a spike becomes a backlog instead of an outage, and services stop depending on each other being up at the same moment. This module covers the three shapes of messaging, what "delivered" really means, ordering, and how to handle messages that keep failing.

1. The idea

A restaurant's order rail. Waiters clip tickets to it as fast as diners order; cooks take tickets as fast as they can cook. A rush doesn't stop waiters taking orders, it just makes the rail longer. If a cook drops a dish, the ticket goes back on the rail. And the rail itself is a record of every order of the night, if you keep the tickets instead of throwing them away.

A producer sends messages; a broker stores them; consumers receive and process them. Messaging buys three things:

Three shapes of messaging

ShapeHow it behavesUse forExamples
Message queueEach message is handled by one consumer, then deleted. Workers compete for messages.Background jobs: send this email, resize this image, charge this cardAmazon SQS, RabbitMQ, Google Cloud Tasks, Redis Streams or BullMQ
Event log (stream)Messages are appended to an ordered, durable log and kept (days, or forever). Each consumer group reads at its own position and can replay.Event-driven systems, change data capture, analytics pipelines, many readers of the same eventsApache Kafka, Amazon Kinesis, Redpanda, Apache Pulsar
Pub/subA message published to a topic is pushed to every current subscriber.Broadcasting: notifications, cache invalidation, real-time updatesGoogle Cloud Pub/Sub, Amazon SNS, Redis Pub/Sub (fire-and-forget)

2. How it works

What "delivered" means

Networks and consumers fail mid-message, so every system picks a delivery guarantee:

In practice: assume duplicates, and make consumers idempotent. Record processed message IDs, use upserts, or use the idempotency keys from Module 07.

Acknowledgements and the visibility timeout

In SQS, receiving a message hides it from other consumers for a visibility timeout (30 seconds by default). If the consumer deletes it in time, it's done; if the consumer crashes, the message reappears and another worker takes it. Set the timeout longer than the slowest normal processing time, or a slow worker and a second worker will process the same message at once.

Partitions, ordering and consumer groups

Kafka splits a topic into partitions. Messages with the same key go to the same partition and stay in order; there's no order across partitions. In a consumer group, each partition is read by exactly one consumer, so the number of partitions caps how many consumers can work in parallel. Each group tracks its offset (position) per partition. Press Play.

Messages that keep failing

A malformed message (a poison message) fails every time and, with naive retries, blocks or burns workers forever. After a few attempts (3–5 is typical), move it to a dead-letter queue (DLQ), alert on it, fix the bug, and replay it. Retries themselves should back off with jitter (Module 24), possibly through separate retry topics with increasing delays.

Back-pressure: when the queue keeps growing

A queue only absorbs bursts. If messages arrive faster than consumers process them on average, the backlog grows without limit, and latency with it. Watch queue depth and consumer lag; scale consumers on them, not on CPU; and if you can't keep up, push back on producers (reject, slow down) or shed low-value work. Try it in section 3.

Scaling writes: the strategies in order

The core idea is to reduce the write throughput each component handles. Do the math first, then go in order:

  1. Vertical scaling and the right database. Check you're actually hitting disk, CPU or network limits; modern machines are huge. For append-heavy data, a write-optimised store (Cassandra, a time-series database) trades read speed for write speed, because it appends instead of updating pages (Module 12).
  2. Sharding and vertical partitioning (Module 15): ten shards ≈ ten times the writes, if the key spreads load; split hot counters from content.
  3. Queues for bursts, load shedding for overload. A queue absorbs a spike and lets the database drain it steadily, but if input stays above capacity on average, the queue grows forever. Load shedding drops the least valuable writes when overloaded: a location ping (a fresher one arrives in seconds), impressions before clicks.
  4. Batching and hierarchical aggregation. Combine many small writes into a few: a "like batcher" sums likes per post per second, turning 100 writes into 1. For live events with millions of viewers, aggregate in layers (below).
PromptWrite-scaling plan
Instagram / socialShard by user ID; separate profiles, posts and analytics; tiered storage for old posts
News feedBalance celebrity fan-out on write against building feeds on read
Search indexingPartition the index and batch the indexing pipeline
Live comments and likesHierarchical aggregation and batching

State the trade-offs: queues add delay and eventual consistency; partitioning can make some reads harder; batching adds latency and moving parts, and loses the batch on a crash unless the source (a Kafka topic) can be re-read.

3. Try it yourself

Messages arrive at a steady rate with a burst in the middle. Workers each handle a fixed number per second. Find the worker count that drains the burst within your target, and see what happens when the average rate exceeds capacity.

200/s
5× for 2 min
5
50/s

4. Trade-offs

Async means "not done yet"

The user gets "accepted", not "finished". The product must show status ("processing…"), and you need a way to report failure later (Module 23 covers job status).

Queue or log?

A queue is simplest for jobs: built-in retries, visibility timeouts and DLQs. A log is better when many consumers need the same events, order per key matters, or you want to replay history.

Ordering costs parallelism

Strict order holds only within one partition (or one FIFO queue group). Order everything and you have one lane. Order only what must be ordered: per user, per account, per order.

Don't queue what should be fast and simple

Putting a 5 ms call behind a queue adds latency, moving parts and failure modes. Queue work that's slow, bursty, optional or must survive failures.

5. In real systems

The technologies up close

The default answer in an interviewAdd a queue when work is slow, bursty or must survive failures, and say three things: the guarantee (at-least-once, so consumers are idempotent), the key and ordering (partitioned by order ID), and the failure path (retries with backoff, then a DLQ we alert on). Don't use a queue to hide a database that can't handle normal load.

Apache Kafka

Situational · the event log at scale

A distributed, durable, append-only log that works as a message queue or an event stream. Lead with your partition key and your plan for hot partitions.

How it works

TermMeaning
BrokerA server in the cluster; more brokers = more storage and throughput
Topic / partitionA topic is split into partitions: ordered, immutable logs, the unit of parallelism and ordering
Keyhash(key) % partitions: the same key always lands in the same partition, in order. No key = spread evenly, unordered
Consumer groupEach partition is read by one consumer in the group; consumers pull at their own pace
OffsetA consumer's position in a partition; committed so it can resume after a crash
  • Queue vs stream: one group processing each message = a queue; many groups reading the same events independently, with replay = a stream. Retention defaults to 7 days.
  • Durability: replication factor 3, acks=all, a minimum in-sync replica count; the controller promotes an in-sync follower if a partition's leader dies.
  • Delivery: at-least-once by default (a crash before committing the offset means reprocessing); idempotent producers stop duplicates on retry; exactly-once within Kafka uses transactions.
  • Failures: a dead consumer's partitions are rebalanced to the others; commit offsets only after the work is safely stored. Consumer retries aren't built in: use a retry topic, then a DLQ.

Status today (checked October 2026)

Kafka 4.3 is current (October 2026). Since 4.0 it runs without ZooKeeper (KRaft), and 4.2 added share groups for queue-style consumption. Managed: Confluent Cloud, Amazon MSK; Redpanda is a compatible alternative. Rough sizing: one broker handles on the order of a terabyte and up to about a million small messages a second, so below that, scaling Kafka isn't worth discussing.

Use it when

  • Async work at scale with ordering per key (all events of one order in sequence)
  • Many independent consumers of the same events, with replay (analytics, search indexing, CDC)
  • Absorbing bursts in front of a slower system

In the interview

Clicks go to a Kafka topic partitioned by ad_id, replication factor 3 with acks=all. Consumers are idempotent because delivery is at-least-once; a viral ad gets a salted key so one partition doesn't melt.

🚩 Red flags
  • No partition key, or a skewed one: a viral ad sends everything to one partition. Fixes: salting (ad_id + random, re-aggregate later), a compound key (ad_id + region), back-pressure.
  • Blobs in Kafka. Store the video in S3, send a pointer.
  • Assuming exactly-once end to end without idempotent consumers.
  • Spending time on "what if Kafka goes down?" A replicated cluster going down entirely is unrealistic; it's fair to say so politely and focus on consumer failures.

Amazon SQS (and simple job queues)

The default for background jobs

A fully managed message queue: send, receive, delete, with retries and dead-letter queues built in.

How it works

A received message becomes invisible for a visibility timeout (30 s by default); delete it when done, or it reappears for another worker. After a set number of receives it moves to a dead-letter queue. Standard queues: near-unlimited throughput, at-least-once, best-effort order. FIFO queues: order per message group, deduplication.

Status today

Mature, serverless, pay per message; messages up to 1 MiB. Google Cloud Tasks and Azure Queue Storage are equivalents.

Use it when

  • Background jobs: send this email, resize this image
  • When you want retries and DLQs without building them

In the interview

Jobs go on SQS with a redrive policy: three failed attempts and the message lands in a DLQ we alert on.

🚩 Red flags
  • A visibility timeout shorter than the job: two workers process the same message.

RabbitMQ

Situational · rich routing

A self-hosted message broker with flexible routing through exchanges (direct, topic, fan-out), plus quorum queues and streams.

How it works

Producers publish to an exchange; bindings route each message to one or more queues; consumers acknowledge each message. Quorum queues replicate with Raft for durability.

Status today

Mature and widely deployed on-premises and as a managed service (Amazon MQ, CloudAMQP).

Use it when

  • Complex routing rules between many services
  • Teams already operating it

In the interview

Fine to name if you know it; otherwise a queue such as SQS or RabbitMQ is enough.

🚩 Red flags
  • Debating Kafka vs RabbitMQ at length. Interviewers care about the guarantee, the key and the failure path, not the brand.

Redis Streams and BullMQ

Situational · modest queues, Redis already there

Queues built on Redis: Streams with consumer groups and acknowledgements, or job libraries like BullMQ with delays, retries and priorities.

How it works

XADD appends; consumer groups read with XREADGROUP and acknowledge; a dead worker's pending items are reclaimed with XAUTOCLAIM. Durability follows Redis's persistence settings.

Status today

Popular for small and medium workloads where Redis is already running.

Use it when

  • Modest job queues and notification fan-out alongside an existing Redis

In the interview

At this volume, Redis Streams is enough; we'd move to Kafka if we needed replay or long retention.

🚩 Red flags
  • Treating a memory-first queue as durable for jobs you can't lose.

Segmented logs and the high-water mark

Situational · how logs work inside

How a replicated log (a Kafka partition, a Raft log) is stored and what readers are allowed to see.

How it works

  • Segmented log: the log is a series of files (segments), each a few hundred MB to 1 GB, plus an index from offset to position. New messages go to the active segment; retention deletes or compacts whole old segments, which is cheap.
  • High-water mark: the highest offset that has reached every in-sync replica. Consumers may only read up to it, so a message they've seen can't vanish if the leader dies before followers copy it.
  • Log compaction keeps only the latest value per key, turning a topic into a durable table of current state.

Status today

Kafka, Redpanda, Pulsar's BookKeeper and Raft implementations all work this way; Kafka exposes it as consumer lag and the in-sync replica set.

Use it when

  • Infrastructure interviews: design a message queue or a replicated log
  • Explaining why acks=all and min.insync.replicas matter

In the interview

Consumers read only up to the high-water mark, so nothing they process can be lost on a leader failover.

🚩 Red flags
  • Assuming consumers see a message the moment the leader writes it. They see it once it's replicated.

6. Remember it

Memory hook: "Clip the ticket, cook it twice safely"Producers clip tickets to the rail and move on; cooks take them at their own pace. Delivery is at-least-once, so cooking the same ticket twice must do no harm.
How this hook works: what "Clip the ticket, cook it twice safely" means

What the slogan means, phrase by phrase

"Clip the ticket"
The producer sends a small message and returns immediately; the broker stores it durably.
"cook it"
A consumer takes it when ready, processes it, and only then acknowledges it.
"twice safely"
Crashes between processing and acknowledging cause redelivery, so processing must be idempotent: a duplicate ticket mustn't produce a second dish.

Why this picture fits: an order rail is a queue you've seen working under pressure: rushes lengthen it, dropped dishes go back on it, and nobody waits at the pass for one cook.

The story → the concept

In the kitchenIn the system
Waiters clipping ticketsProducers sending messages
The railThe broker: a queue or a log
Cooks taking ticketsConsumers (competing workers)
A ticket back on the rail after a dropped dishRedelivery after a failed or unacknowledged message
A ticket nobody can read, set aside for the managerA dead-letter queue
Keeping every ticket of the nightAn event log with retention and replay

Watch it on an example

  1. Checkout publishes OrderPlaced {order_id: 881} and returns "Order confirmed" in 40 ms.
  2. The email worker sends the receipt, then crashes before acknowledging. The message is redelivered.
  3. The second attempt checks "receipt already sent for order 881?" and skips: no duplicate email.
  4. The analytics consumer reads the same event from the log an hour later, at its own pace.

The trap it saves you from

Believing a broker's "exactly-once" claim covers your side effects. Emails, payments and API calls outside the broker are only safe if your handler is idempotent.

It still works when…

The "messages" are database changes (CDC, Module 13) or workflow steps (Module 22): same rail, same rule.

7. Common mistakes

Non-idempotent consumersAt-least-once delivery will deliver duplicates. Without idempotency, they become double charges and duplicate emails.
Using a queue to hide a consumer that can't keep upIf the average arrival rate exceeds capacity, the backlog grows forever. Scale consumers, or push back.
No dead-letter queueOne poison message retried forever wastes workers and can block a partition. Cap attempts and park it.
Ordering on everythingA single partition or FIFO group for all messages caps throughput at one consumer. Order per key instead.
Big payloads in messagesFiles and large documents bloat brokers and slow everyone. Store them in object storage and send a reference.

8. Design exercise (25 minutes)

  1. Pick the shape (queue, log or pub/sub) for: sending password-reset emails, streaming every order to analytics and search, telling all app servers to drop a cached setting, processing video uploads.
  2. Design an idempotent consumer for "charge the customer for order X". What do you store, and what happens on a duplicate?
  3. Choose keys: a bank streams account transactions through Kafka. What key guarantees per-account order? How many partitions for 50,000 events a second if one consumer handles 2,000?
  4. Size the workers in the simulator for 300 messages a second normally and a 10× burst, drained within 10 minutes.
  5. Write it down: the three delivery guarantees, and "assume duplicates".

9. Self-check quiz

With at-least-once delivery, what must consumers handle?

A crash between processing and acknowledging causes redelivery, so the same message can be processed twice.

Where does Kafka guarantee message order?

Messages with the same key go to the same partition, in order. There's no order across partitions.

What caps the parallelism of one consumer group in classic Kafka?

Each partition goes to one consumer in the group. (Share groups in Kafka 4.2+ relax this for queue-style use.)

A message fails every time it's processed. What should happen?

Park it where it can be inspected and replayed after a fix, and alert so someone looks.

Writes arrive at 30k/s on average and your database handles 20k/s. Does a queue in front fix it?

Queues absorb bursts, not a sustained excess. Shard, batch or shed load instead.

Clicks for one viral ad all land on one Kafka partition. What's the usual fix?

One key always maps to one partition, which only one consumer in a group can read. Spreading the key spreads the load.

10. Go deeper

ResourceTimeWhy
Turning the database inside out (Martin Kleppmann)45 minThe talk and essay on building systems around an append-only log of events, the idea behind Kafka-centred designs.
Apache Kafka documentation, design section1 hPartitions, replication, consumer groups and delivery semantics from the source.
Amazon SQS Developer Guide45 minVisibility timeouts, DLQs, FIFO queues and limits in a managed queue.
Designing Data-Intensive Applications, chapter 11 (Stream Processing)2 hMessage brokers vs logs, and how streams connect systems.

11. Under the hood (optional)

How queues and logs really deliver, order, retry and store messages. Not needed for most interviews; open it when you want to know how the products really do it. All the deep dives are listed on the Under the hood page.

Messaging internalsQueues vs logs, delivery guarantees with a crash lab, ordering scopes, rebalancing, visibility timeouts and DLQs, segments and compaction, partition sizing, 8 products comparedabout 40 min

"Put it on a queue" hides real choices: does the message disappear once read or stay for replay, who decides how fast consumers receive, what happens when a consumer dies halfway, and what "exactly once" can honestly mean. Products answer these differently, and the differences shape designs.

Queue vs log

Queue (SQS, RabbitMQ classic and quorum queues)Log (Kafka, Kinesis, Pulsar, RabbitMQ streams, Redis Streams)
After a message is handledDeletedKept until retention expires; each consumer group tracks its own position (offset)
Many independent consumersNeed a copy each (fan-out through topics or exchanges)Free: each group reads the same log
Replay after a bug fixGoneRewind the offset and reprocess
ParallelismAny number of workers take messages one by oneOne consumer per partition per group (Kafka share groups, GA in 4.2, add queue-style consumption)
Per-message acks and retriesNativeA committed offset covers everything before it; one stuck message blocks its partition

Push vs pull: RabbitMQ pushes messages to consumers, limited by a prefetch count of unacknowledged messages; Kafka and SQS consumers pull at their own pace (SQS long polling waits up to 20 seconds for messages). Pull makes back-pressure natural; push gives lower latency but needs the prefetch limit to avoid flooding slow consumers.

Delivery guarantees and "exactly once"

Whether a message is lost or processed twice depends on when the consumer acknowledges relative to its side effects, and where a crash lands. Try it:

  • Exactly-once delivery over a network is impossible in general; exactly-once effect is achievable by making processing idempotent or by committing output and progress atomically.
  • Inside Kafka: idempotent producers (on by default since 3.0) remove duplicates from producer retries; transactions write output records and the consumer's offsets atomically, so read-process-write within Kafka is exactly once. Effects outside Kafka (an email, a payment) still need idempotency.
  • SQS FIFO deduplicates sends with the same deduplication ID within 5 minutes; Pub/Sub offers an exactly-once delivery option for pull subscriptions.

Ordering scopes

ProductOrder is guaranteed withinPick the key that must stay in order
Kafka, Kinesis, PulsarOne partition or shardThe message key (an order ID, a user ID) chooses the partition
SQS FIFOOne message group IDGroup by entity; different groups run in parallel
Pub/SubOne ordering key (when ordering is enabled)Same idea
RabbitMQOne queue with one consumerSeveral consumers or redeliveries reorder messages
SQS standardNothing (best effort)Design consumers not to care, or use FIFO

Retries break order too: if message 5 fails and goes to a retry queue while 6 succeeds, the entity sees 6 before 5. Where order matters per entity, pause that key or use a version check on the receiving side.

Consumer groups and rebalancing

  • Partitions are divided among a group's members; a consumer that joins, leaves or misses heartbeats triggers a rebalance.
  • The classic ("eager") protocol stopped the whole group while every partition was revoked and reassigned. Cooperative rebalancing moved only the partitions that had to move, and Kafka 4.0 made the broker-driven protocol from KIP-848 generally available, so assignments change incrementally without a group-wide pause.
  • A consumer that processes too slowly between polls (max.poll.interval.ms, 5 minutes by default) is considered dead and its partitions are taken away, so long jobs need smaller batches or a pause on the partition.

Acks, visibility timeouts, retries and dead letters

  • SQS: a received message is hidden for the visibility timeout (30 seconds by default, up to 12 hours); if not deleted in time it reappears for another consumer. Extend the timeout for long jobs. After maxReceiveCount receives, a redrive policy moves it to a dead-letter queue.
  • Pub/Sub uses an ack deadline the same way; RabbitMQ redelivers unacknowledged messages when the consumer's channel closes, and quorum queues can cap redeliveries.
  • Poison messages (that always fail) must leave the main path: a dead-letter queue, with alerts and a tool to inspect and replay.
  • Retry with delay in a log: Kafka has no per-message delay, so failed messages go to retry topics (retry-1m, retry-10m) read by consumers that wait until each message is due.
  • Consumer lag (newest offset minus committed offset, or the age of the oldest message) is the metric to alert and autoscale on.

Inside a log: segments, retention, compaction and speed

  • Segments: each partition is a series of files (1 GB by default in Kafka) with offset and timestamp indexes. Retention deletes whole old segments by time or size, which is cheap. Tiered storage (Kafka 3.9+, Pulsar) moves old segments to object storage.
  • Compaction keeps only the latest record per key (a tombstone, a null value, deletes the key): a topic becomes a changelog you can rebuild state from.
  • Why it's fast: producers batch records (linger.ms, batch.size) and compress whole batches; brokers append sequentially and serve reads from the operating system's page cache, sending file bytes straight to the socket with zero-copy sendfile.
  • Schemas: a schema registry (Avro, Protobuf, JSON Schema) checks that new producer versions stay compatible (backward: new readers read old data; forward: old readers read new data), so producers and consumers can deploy independently.
  • Across regions: MirrorMaker 2, Confluent Cluster Linking, Pulsar geo-replication; offsets differ between clusters, so consumers fail over by timestamp or translated offsets.

How many partitions?

Product by product

ProductModelDelivery and orderingThe detail worth knowing
Apache KafkaPartitioned log on brokers; consumers pull; KRaft metadata (no ZooKeeper since 4.0)Order per partition; idempotent producers and transactions for exactly-once inside KafkaShare groups (4.2) add queue-style consumption; compaction keeps the latest value per key
Apache PulsarStateless brokers over BookKeeper storageExclusive, shared, failover and key-shared subscriptionsStorage scales separately; geo-replication and tiered storage built in
RabbitMQExchanges route to queues; push with prefetchQuorum queues (Raft-replicated); streams for log-style useRouting lives in the broker; classic mirrored queues were removed in 4.0
Amazon SQSManaged queue; long pollingStandard: at-least-once, best-effort order; FIFO: ordered per group, 5-minute deduplicationVisibility timeout and redrive to a dead-letter queue
Google Pub/SubGlobal topics and subscriptions, push or pullOrdering keys; exactly-once delivery option; seek to replayAck deadlines work like visibility timeouts
Kinesis Data StreamsShards with fixed capacity (1 MB/s or 1,000 records/s in, 2 MB/s out each)Order per shardEnhanced fan-out gives each consumer its own read capacity
NATSCore pub/sub; JetStream adds persistent streamsAt-most-once in core; at-least-once and deduplication windows in JetStreamVery low latency; wildcard subjects
Redis StreamsAppend-only stream in memoryConsumer groups with a pending list per consumer; XAUTOCLAIM takes over stuck messagesGood for modest volumes already on Redis

Say it in the interview

Answers that show depth
  • "Do you get exactly-once?" Delivery is at-least-once; the consumer makes the effect exactly-once by deduplicating on the message ID in the same transaction as its write (or Kafka transactions when input and output are both Kafka).
  • "Why not more consumers?" In a Kafka group, consumers beyond the partition count sit idle; partitions cap parallelism, so size them for peak (or use share groups).
  • "What about a message that always fails?" Bounded retries with backoff, then a dead-letter queue with an alert and a replay tool, so it can't block the partition.

A consumer processes a message, then crashes before acknowledging it. What happens?

The broker redelivers it. Make processing idempotent.

Kafka guarantees order within what?

The message key picks the partition, so order holds per key.

An SQS consumer takes longer than the visibility timeout. What happens to the message?

Extend the timeout for long jobs, or the work runs twice.

Sources