Course · 09

How do services talk without waiting for each other?

Message queues, publish-subscribe and Kafka, from zero to interview depth. Every number is checked on kafka.apache.org (4.x docs), AWS, RabbitMQ, Redis and Google Cloud docs, checked 2026-10-25.

TL;DR

A broker is a middleman program: the sender drops a message and moves on, and the receiver picks it up when it is ready.

The whole idea in one picture A sender app gives a message to a middleman (the broker), which hands it to one or more receiver apps, so the sender and receivers never talk directly. SEND Sender appproducer The middlemanbroker Receiver Aconsumer Receiver Bconsumer LEGEND Sends Middleman Receives
The sender never talks to the receivers directly.
  • Queue = each message goes to one worker (a task). Pub/sub = every subscriber gets a copy (an event).
  • Kafka is a kept log, split into partitions. Order holds only inside one partition. Readers move a bookmark (offset); nothing is deleted on read.
  • Most brokers deliver at least once. So your consumer must be safe to run twice (idempotent).
  • Durability in Kafka = acks=all + replication factor 3 + min.insync.replicas=2. The defaults alone are weaker.
168hours
Kafka default retention (log.retention.hours), 7 days
1
consumer per partition inside one group. Extra consumers sit idle.
30s
SQS default visibility timeout (max 12 h)
20s
SQS long-poll maximum wait

—Words you need

Each hard word once, in plain words glossary

Message / event
A small packet of data. An event says "this happened".
Producer
The program that writes messages.
Consumer
The program that reads messages.
Broker
The middleman program that stores and hands out messages.
Queue
A waiting line. Each message goes to one reader.
Topic
A named stream of messages. Many readers can read it.
Partition
One slice of a Kafka topic. An ordered, append-only list.
Offset
A message's position number in a partition. Also the reader's bookmark.
Consumer group
A team of readers that split one topic's partitions.
Rebalance
Re-sharing partitions when a reader joins or leaves.
Replica / ISR
A copy of a partition. ISR = copies that are fully caught up.
Ack
"Got it" reply. Lets the other side forget or move on.
Idempotent
Doing it twice gives the same result as doing it once.
Dead-letter queue (DLQ)
A side shelf for messages that keep failing.
Consumer lag
How many messages the reader is behind the newest one.
Backpressure
Slowing the sender down when the receiver cannot keep up.

Why and what

01Why async messaging at all

Load levelling, decoupling, fault isolation core

Direct call versus a queue Without a middleman, the app's call fails when the busy service is down; with a queue, the app puts the message in the waiting line and the service takes it later. WITHOUT A MIDDLEMAN WITH A QUEUE CALL FAILS PUT TAKE Your apphas to wait Busy servicedown or slow Your appmoves on Waiting linequeue Busy serviceat its own pace LEGEND Your app Queue Other service
A direct call needs both sides awake at the same moment. A queue does not.

A direct call is synchronous (the caller waits for the answer). If the callee is slow, the caller is slow. If it is down, the caller fails too. Asynchronous messaging (send now, handled later) breaks that chain.

BenefitWhat it meansExample
Load levellingThe queue soaks up a spike. Workers drain it at a steady speed.Black Friday orders pile up; 10 workers still process them.
DecouplingSender does not know who reads, or when. New readers need no sender change.Add a fraud checker to "order placed" without touching checkout.
Fault isolationA dead reader does not break the sender. Messages wait until it is back.Email service is down for 10 minutes; no order fails.
Independent scalingAdd workers only where the work is.Scale image resizers, not the upload API.
In everyday words
A letterbox. The postman does not wait for you to open the door. He drops the letter and goes.
Actually
Azure names this Queue-Based Load Leveling: a queue "acts as a buffer between a task and the service that it invokes", so "you only need enough service instances to meet the requirements for an average load rather than the peak load". The cost: if producers are faster than consumers on average, "the queue continues to grow". Async also costs you: no instant answer, harder debugging, and duplicates to handle.

Use a queue when the caller does not need the answer right now. Keep a direct call when it does.

Source: Azure: Queue-Based Load Leveling

02Point-to-point queue vs publish-subscribe

One reader, or a copy for everyone interview favourite

Queue: one message, one worker

A queue with two workers A sender puts messages in a waiting line; two workers share the line and each message is taken by only one worker. Senderproducer The waiting line 4 3 2 1 queue Worker 1takes note 1 Worker 2takes note 2 LEGEND Queue Worker / one message
Two workers share one line. Message 1 goes to Worker 1 only, never to both.

Many workers on one queue is the Competing Consumers pattern. Each message is done once, by whoever is free.

Pub/sub: a copy for every subscriber

One message, a copy for each subscriber A sender posts one message to a notice board (topic), and three different subscriber apps each receive their own copy. Senderpublisher Notice boardtopic Email appgets a copy Billing appgets a copy Analytics appgets a copy LEGEND Topic Subscriber
The sender does not know who is listening. Adding a fourth app changes nothing for it.
In everyday words
A queue is a bank token line: each token goes to one counter. Pub/sub is a WhatsApp group: everyone gets the message.
Actually
Enterprise Integration Patterns: a Point-to-Point Channel "ensures that only one receiver will receive a particular message". A Publish-Subscribe Channel "delivers a copy of a particular event to each receiver". Azure adds that pub/sub isolates faults: "a subscriber failure doesn't affect the publisher or other subscribers". Kafka gives you both at once: a queue inside a consumer group, pub/sub across groups (section 05).

Task to be done once → queue. Fact many teams care about → pub/sub.

Source: EIP: Point-to-Point · EIP: Publish-Subscribe · Azure: Competing Consumers · Azure: Publisher-Subscriber

03Message broker vs ESB

Smart endpoints, dumb pipes core

A broker (Kafka, RabbitMQ, SQS) stores and moves messages. An ESB (Enterprise Service Bus) also rewrites them, translates protocols and holds business rules in the middle.

An ESB sits in the middle and translates App A sends one message to a central smart hub, which routes it, rewrites its format and converts the protocol so App B (SOAP/XML) and App C (REST/JSON) can each read it. App Asends once The smart hub route · rewrite translate protocol ESB App Bspeaks SOAP/XML App Cspeaks REST/JSON LEGEND ESB (logic lives here) Connected app
In an ESB the business rules live in the middle. With a broker they live in the apps.
In everyday words
A broker is a post office that moves sealed letters. An ESB opens your letter, translates it, and decides who else should get it.
Actually
IBM: an ESB "performs transformations of data models, handles connectivity, performs message routing, converts communication protocols". It became a bottleneck: teams "found themselves waiting in line for their integrations". Microservices chose "smart endpoints and dumb pipes" (Lewis & Fowler, 2014): the pipe only moves bytes, the services hold the logic.

In an interview, keep the pipe dumb. Put routing and rules in the services.

Source: IBM: What is an ESB · Lewis & Fowler: Microservices

Kafka in depth

04Kafka: topic, partition, offset, key

A log you append to and read with a bookmark deep dive

A Kafka topic split into partitions A topic is split into three partitions of numbered messages; a consumer group of two consumers shares them, each partition read by exactly one consumer, which keeps a bookmark (offset) of the next message to read. TOPIC · 3 PARTITIONS CONSUMER GROUP P0 0 1 2 3 4 5 P1 0 1 2 P2 0 1 2 3 Consumer 1reads P0, P1 Consumer 2reads P2 LEGEND Already read Bookmark (offset) Waiting
Example: 3 partitions, a group of 2 consumers. Each partition has one reader and one bookmark.

A topic is split into partitions. Each partition is an append-only file (a log). Each message gets the next offset number. Partitions live on different brokers, so a topic can grow past one machine.

The key decides the partition. Same key, same partition, so same order. No key: the producer spreads messages across partitions.

How a producer picks a partition (default partitioner)
key != null  →  partition = murmur2(keyBytes) % numPartitions   // docs: "a hash of the key"
key == null  →  "sticky": fill a batch for one partition, then switch
add partitions later  →  hash % N changes  →  old keys move to new partitions
In everyday words
A notebook torn into chapters. Every note about customer 42 goes into the same chapter, so you read them in order.
Actually
Kafka docs: "Events with the same event key … are written to the same partition, and Kafka guarantees that any consumer of a given topic-partition will always read that partition's events in exactly the same order as they were written." Messages "are not deleted after consumption". Order across partitions is not guaranteed. The hash is murmur2 in the source code (BuiltInPartitioner.java); the docs only say "a hash of the key". Adding partitions breaks key-to-partition mapping for existing keys, so pick the count up front.

Need order per customer? Key by customer id. Need global order? One partition, and you lose parallelism.

Source: Kafka: Introduction · Kafka design · KIP-794 sticky partitioner

05Consumer groups, rebalancing, share groups

Queue inside a group, pub/sub across groups interview favourite

More consumers than partitions A topic with two partitions is read by a group of three consumers; partition 0 goes to consumer 1, partition 1 to consumer 2, and consumer 3 gets nothing and sits idle. TOPIC · 2 PARTITIONS ONE GROUP ASSIGNED ASSIGNED Partition 0orders-0 Partition 1orders-1 Consumer 1reads P0 Consumer 2reads P1 Consumer 3idle: no partition LEGEND Partition Busy consumer Idle consumer
The partition count is the ceiling on parallel readers in one group.

Inside one group, each partition has exactly one reader. That is a queue. Each group keeps its own offsets, so two groups both get every message. That is pub/sub.

SetupWhat happens
6 partitions, 3 consumers in a groupEach consumer reads 2 partitions.
6 partitions, 6 consumersOne each. Maximum parallelism.
6 partitions, 8 consumers2 consumers idle. More readers does not help.
2 groups ("billing", "email") on one topicBoth groups get every message.

Rebalancing

A rebalance re-shares partitions when a consumer joins, leaves, or misses heartbeats. In the old "eager" way, everyone stopped reading during it. Two fixes came later:

  • Cooperative rebalancing (KIP-429, Kafka 2.4): only the partitions that move are paused.
  • New consumer protocol (KIP-848, group.protocol=consumer): the broker plans the assignment, step by step, with no group-wide stop. GA in Kafka 4.0. The client default is still classic: you opt in.

A consumer that does not call poll() within max.poll.interval.ms (default 300000 ms, 5 minutes) is kicked out and its partitions move. Slow handlers cause "rebalance storms".

Share groups: Kafka as a real queue

A normal group cannot have more readers than partitions. Share groups (KIP-932, "Queues for Kafka") remove that limit. Many consumers read the same partition. Each record is acked one by one, and has a delivery count. Order is not kept.

In everyday words
A group is a team splitting the chapters of one book. Two teams each read the whole book. A share group is a team that grabs pages from the same chapter.
Actually
Kafka docs: "each partition is consumed by exactly one consumer within each subscribing consumer group". For share groups: "partitions may be assigned to multiple consumers", "records are acknowledged individually", and "delivery attempts … are counted". Release status: early access in Kafka 4.0, preview in 4.1, "production-ready" in 4.2 (February 2026). Defaults: delivery-count limit 5, record lock 30 s. The docs say it directly: "The number of consumers in a share group can exceed the number of partitions".

Partitions = max parallel readers per group. Plan partitions for your peak reader count.

Source: Kafka design: consumer position · KIP-848 · KIP-932 · consumer configs

06Retention vs log compaction

Keep by time, or keep the latest per key deep dive

Log compaction keeps the latest value per key Before compaction the log holds six writes for keys K1, K2 and K3; after compaction only the newest record per key stays, at its original offset, and a null value for K2 marks it as deleted. BEFORE · EVERY WRITE KEPT COMPACT AFTER COMPACTION K1=aoff 0 K2=boff 1 K1=coff 2 K3=doff 3 K2=nulloff 4 K1=eoff 5 gone gone gone K3=doff 3 K2=nulloff 4 K1=eoff 5 LEGEND Record kept Removed (older value) Latest for K1
Offsets never change. K2=null is a "tombstone": it says "K2 is deleted", and is itself removed later.
PolicyKeepsUse it forKey setting
cleanup.policy=delete (default)Everything, until it is older than the limitEvent streams: clicks, orders, logsretention.ms / log.retention.hours = 168
cleanup.policy=compactThe newest record for each key, foreverCurrent state: user profile, config, a changelog for a tabledelete.retention.ms = 86400000 ms, 1 day (how long tombstones stay)
compact,deleteNewest per key, but also dropped when oldState that may expireBoth
In everyday words
Retention is a CCTV tape that wipes after 7 days. Compaction is your phone's contact list: only the latest number for each person.
Actually
Kafka docs: compaction "ensures that Kafka will always retain at least the last known value for each message key within the log of data for a single topic partition". A record with a null value is a delete marker. The log is cleaned in the background, so old values may stay a while; "at least the last value" is the promise, not "only".

Compacted topics turn Kafka into a replayable copy of a table. A new service can rebuild its state from offset 0.

Source: Kafka: log compaction · broker configs

07Replication, ISR and acks

How Kafka avoids losing a write interview favourite

A write with acks=all The producer writes to the partition leader; the leader copies to two followers; one follower is caught up and in the in-sync replica set, the other lags and is out of it; the leader acks once every in-sync replica has the write. WRITE ACK Produceracks=all Leaderbroker 1 · ISR Followerbroker 2 · ISR Followerb3 · lagging LEGEND Leader In sync (ISR) Out of sync
Replication factor 3, ISR = {1, 2}. The ack waits for broker 2, not for lagging broker 3.

Every partition has one leader and some followers. All reads and writes go to the leader. Followers copy from it. The replication factor (RF) is the total number of copies. The ISR (in-sync replicas) is the set of copies that are caught up.

acksProducer waits forSpeedCan lose data when
0NothingFastestAny failure. No retry possible.
1The leader's own writeFastLeader dies before a follower copies it.
all (-1)Every replica in the ISRSlowestOnly if the ISR has shrunk to the leader alone. Fix with min.insync.replicas.

min.insync.replicas sets how small the ISR may get before writes with acks=all are refused (the producer gets NotEnoughReplicas). The classic durable setup:

The durable recipe
replication.factor   = 3      // 3 copies on 3 brokers
min.insync.replicas  = 2      // a write needs 2 copies, else it is refused
acks                 = all    // producer waits for the whole ISR
unclean.leader.election.enable = false   // never elect an out-of-sync copy

With these, one broker can die and writes go on. Two die: writes stop, but nothing acked is lost. That is a choice of consistency over availability (see page 08, CAP).

In everyday words
A teacher (leader) writes on the board; two students copy it. "acks=all" means you wait until every student who is keeping up has copied it.
Actually
Defaults (Kafka 4.x configs): producer acks = all (since 3.0, KIP-679); broker min.insync.replicas = 1; default.replication.factor = 1; unclean.leader.election.enable = false; a follower drops out of the ISR if it has not caught up for replica.lag.time.max.ms = 30000 ms. So acks=all with the default min.insync.replicas=1 can still ack a write held by one broker only.

"acks=all" alone is not durable. Pair it with RF 3 and min.insync.replicas 2.

Source: Kafka design: replication · producer configs · broker configs

08Idempotent producer and transactions

Exactly-once, but only inside Kafka deep dive

A retry that does not create a duplicate The producer sends batch number 5, the broker writes it but the ack is lost; the producer retries batch 5, and the broker sees it already has sequence 5 for this producer id, so it acks without writing it again. SEND SEQ 5 ACK LOST RETRY SEQ 5 ACK · DUPLICATE ProducerPID 7 Brokerlast seq: 5 LEGEND Message Lost on the way Safe answer: no 2nd copy
Producer id + sequence number let the broker spot a resend.

A producer retries when it gets no ack. But maybe the write worked and only the ack was lost. Without protection, that retry writes a duplicate.

The idempotent producer fixes this. The broker gives each producer an id (PID). The producer numbers each batch per partition. The broker drops a batch it has already seen.

Transactions go further. With a transactional.id, a producer can write to several partitions and commit consumer offsets as one all-or-nothing step. Readers with isolation.level=read_committed only see committed data. This is how Kafka Streams does "read, process, write" exactly once.

Exactly-once read-process-write, in outline
producer.initTransactions()                 // transactional.id = "billing-1"
loop:
  records = consumer.poll()
  producer.beginTransaction()
  producer.send(outputTopic, process(records))
  producer.sendOffsetsToTransaction(offsets, groupMetadata)   // offsets inside the same tx
  producer.commitTransaction()              // all or nothing
In everyday words
Each cheque has a number. If the bank sees cheque 5 twice, it pays it once.
Actually
Defaults (Kafka 4.x): enable.idempotence = true (since 3.0); it needs acks=all and max.in.flight.requests.per.connection ≤ 5. Consumer isolation.level default = read_uncommitted (it even returns aborted messages). Kafka's exactly-once covers writes into Kafka. If your consumer also writes to a database or sends an email, Kafka cannot undo that: you still need an idempotent consumer (section 09).

Say it precisely in interviews: "exactly-once inside Kafka; effectively-once end to end with idempotent consumers".

Source: Kafka design: message delivery semantics · producer configs · consumer configs · KIP-98

Reliability

09Delivery promises: lost, twice, or once

At-most-once, at-least-once, exactly-once interview favourite

Bookmark first or work first If the reader saves its bookmark before doing the work, a crash loses the message (at-most-once); if it does the work before saving the bookmark, a crash makes it do the work twice (at-least-once), which is the order our llm_router uses. AT-MOST-ONCE · BOOKMARK FIRST AT-LEAST-ONCE · WORK FIRST Read notefetch Save bookmarkcommit offset Do the workcrash = lost Read notefetch Do the workcrash = do again Save bookmarkour llm_router LEGEND Where a crash hurts What our code does
Only the order of two steps decides the promise.

A consumer does two things: the work, and saving its bookmark (commit the offset, or delete the message). Only their order decides the promise.

PromiseOrderOn a crashFine for
At-most-onceBookmark, then workMessage lostMetrics, live location pings
At-least-onceWork, then bookmarkMessage repeatedAlmost everything (the default)
Exactly-once (effective)At-least-once + skip repeatsRepeat arrives, is ignoredPayments, counters, emails

The idempotent consumer

Make the work safe to repeat. Three common ways:

  • Dedup table: store each message id in the same database transaction as the work. Seen it? Skip.
  • Natural idempotency: "set balance to 50" is safe to repeat. "Add 10" is not.
  • Upsert with a version: only write if the incoming version is newer.
Dedup in the same transaction as the work
BEGIN;
INSERT INTO processed_messages(message_id) VALUES ('evt-981');   -- unique key: fails on a repeat
UPDATE accounts SET balance = balance + 10 WHERE id = 42;
COMMIT;                     -- then commit the Kafka offset / delete the SQS message
In everyday words
Tick the to-do item before or after doing it? Tick first and get interrupted: the job is forgotten. Do first: you may do it twice, so make doing it twice harmless.
Actually
Kafka design docs: "At most once — Messages may be lost but are never redelivered. At least once — Messages are never lost but may be redelivered." Azure's Idempotent Consumer pattern: "processing the same message more than once has the same effects as processing it once". microservices.io: "record the IDs of processed messages in the database".
Seen in our code: llm_router's Kafka consumers
llm_router reads user_events, group_events and subscription_events with autoCommit: false. It commits the offset in a finally block, so the bookmark moves on even when the handler throws. A crash before the commit means a redelivery (at-least-once). A handled error means no retry: it is logged, counted and skipped. There is no DLQ ("No producer is created in v1 — DLQ is deferred", kafka-client.ts:9). This is safe only because each event is a full snapshot, so the next one repairs the data (iaam-consumer.ts:275-286).

No broker gives you exactly-once for free. Choose the order of "do" and "tick", then make "do" safe to repeat.

Source: Kafka: delivery semantics · Azure: Idempotent Consumer · microservices.io · llm_router/src/tc-monetization/

10Poison messages, DLQs, lag and backpressure

What goes wrong when the line backs up deep dive

One bad message blocks a partition The consumer is stuck retrying the message at offset 1, so offsets 2 to 4 behind it wait; after N tries the bad message is parked on a dead-letter topic and the line moves again. PARTITION 0 READ AFTER N TRIES offsets 2, 3, 4 wait behind it 4 3 2 1 0 Consumerstuck on offset 1 Problem shelfdead-letter topic LEGEND Done Poison message Waiting
Kafka keeps order per partition, so a stuck message stalls everything behind it: head-of-line blocking.
Poison message
A message that always fails (bad format, a bug). Retrying forever never helps.
Dead-letter queue
After N tries, park it on a side queue or topic. Alert a human. Replay after the fix.
Head-of-line blocking
In Kafka one stuck record blocks its whole partition. In SQS standard, other messages flow past it.
Retry topics
Move failures to orders.retry.1m, orders.retry.10m, then orders.dlq. Main line keeps moving, but per-key order breaks.

Consumer lag

Lag = newest offset in the partition (log end offset) minus the group's committed offset. It is the number one health number for a consumer. Growing lag means readers are too slow: add consumers (up to the partition count), speed up the handler, or batch.

$ kafka-consumer-groups.sh --bootstrap-server b1:9092 --describe --group billing
TOPIC   PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG
orders  0          1040            1052            12

Backpressure: push vs pull

ModelWho controls speedExamplesOverload protection
PullConsumer asks when readyKafka, SQS, Redis StreamsNatural: a slow reader just falls behind (lag)
PushBroker sends as messages arriveRabbitMQ (basic.consume), SNS, Google Pub/Sub pushPrefetch limit / flow control, or the reader drowns
In everyday words
A parcel nobody can deliver goes to the "return to sender" shelf after 3 attempts, so the van can finish its round.
Actually
Kafka chose pull on purpose: with push, a slow consumer gets overwhelmed; with pull, it "simply falls behind and catches up when it can". RabbitMQ limits push with basic.qos prefetch; its docs say "values in the 100 through 300 range usually offer optimal throughput". SQS: maxReceiveCount (1–1,000) receives before a message moves to the DLQ. RabbitMQ quorum queues have a delivery limit, default 20 since 4.0. Kafka has no built-in DLQ for plain consumers: you build it (Kafka Connect has one for connectors).

Every consumer needs three answers: how many retries, where failures go, and which lag number pages you.

Source: Kafka: push vs pull · RabbitMQ: confirms and prefetch · RabbitMQ quorum queues · SQS DLQ · EIP: Dead Letter Channel

Other brokers

11SQS, SNS, RabbitMQ, Redis, Google Pub/Sub

Same job, different promises core

Amazon SQS: the managed queue

Life of an SQS message A sender puts a message in an SQS queue, a worker takes it and must delete it before the visibility timeout ends; if not deleted it goes back in line, and after too many tries it moves to a dead-letter queue. TAKE BACK IN LINE AFTER N TRIES SenderSendMessage SQS queuekeeps 4 days Workerdelete in 30 s Problem shelfdead-letter queue LEGEND Queue Dead-letter queue Retry path
A message that keeps failing is moved aside, so it stops coming back forever.
SQSStandardFIFO
DeliveryAt-least-onceExactly-once processing (5-minute dedup window)
OrderBest effortStrict, per message group id
Throughput"Nearly unlimited"300 calls/s per action, 3,000 msg/s with batching (high-throughput mode: much more)
DLQStandard queueMust also be FIFO
  • Visibility timeout: a received message is hidden, default 30 s, max 12 h. Not deleted in time → it comes back.
  • Long polling: WaitTimeSeconds up to 20 s. Fewer empty replies, lower cost. Short polling only samples some servers.
  • Retention default 4 days (60 s to 14 days). Max message 1 MiB. Up to 10 messages per receive. Delay up to 15 min.

SNS fan-out: pub/sub in front of queues

Publish once to an SNS topic. Each subscribed SQS queue gets its own copy. Each service drains its own queue at its own pace. Filter policies let a subscriber take only some messages.

RabbitMQ: a post office with sorting rules

Direct exchange
Routing key must match exactly. One address.
Fanout exchange
Copy to every bound queue. Pub/sub.
Topic exchange
Pattern match like order.*.eu. Pub/sub with filters.
Headers exchange
Routes on message headers, not the key.

RabbitMQ pushes to consumers and removes a message once it is acked. Quorum queues replicate with Raft. Streams are an append-only log, closer to Kafka.

Redis Pub/Sub vs Redis Streams

Redis featureStored?PromiseUse for
Pub/Sub (PUBLISH / SUBSCRIBE)No. Nobody listening = lost.At-most-onceLive chat typing dots, cache-invalidation hints
Streams (XADD / XREADGROUP)Yes, a logAt-least-once with XACK; unacked entries sit in the pending list (XPENDING), and XAUTOCLAIM hands them to another readerSmall Kafka-like jobs when you already run Redis

All of them, side by side

SystemShapeDefault promiseKept after read?Push / pullKey numbers
SQS standardQueueAt-least-onceNo (consumer deletes)PullKeep 4 d (max 14), hide 30 s (max 12 h)
SQS FIFOQueueExactly-once processingNoPull300 / 3,000 per s
SNSPub/subFan-outNoPushFilter policies
KafkaKept logAt-least-once; exactly-once inside KafkaYesPullRetention 168 h default
RabbitMQExchanges → queuesAt-least-once with manual acksNo (classic/quorum), Yes (streams)PushPrefetch 100–300
Redis Pub/SubPub/subAt-most-onceNoPushLost if no listener
Redis StreamsKept logAt-least-onceYesPullPending entries list
Google Pub/SubPub/subAt-least-once, no order by defaultUntil ackedBothAck deadline 10 s (max 600), keep 7 d (max 31)
In everyday words
They all "hold and hand over". They differ in whether they keep the note, who gets copies, and what happens when a reader fails.
Actually
AWS: "The maximum is 1,048,576 bytes (1 MiB)" (raised from 256 KiB in August 2025). FIFO "deliver each message once and keep it available until you process and delete it". High-throughput FIFO numbers differ between two AWS pages (30,000 TPS on the queue-types page vs up to 70,000 TPS in some Regions on the quotas page), so treat them as not settled. Redis: "Redis' Pub/Sub exhibits at-most-once message delivery semantics." Google: "By default, Pub/Sub offers at-least-once delivery with no ordering guarantees"; exactly-once delivery exists for pull subscriptions only.

On AWS: SQS for tasks, SNS + SQS for fan-out, Kafka (MSK) or Kinesis when you need replay and per-key order.

Source (checked 2026-10-25): SQS quotas · SQS queue types · long polling · SNS to SQS · RabbitMQ AMQP concepts · Redis Pub/Sub · Redis Streams · Google Pub/Sub

Design choices

12Kafka or a queue? The decision table

The "event vs task" rule interview favourite

The rule: a task ("resize this image", "send this email") must be done once by someone, then it is finished → a queue. An event ("order 42 was placed") is a fact many teams may want, now or later → a log like Kafka.

SituationPickWhy
Background jobs, one worker each, no replaySQS / RabbitMQPer-message ack, retries, DLQ built in. Scale workers freely.
Many teams react to the same eventKafka, or SNS + SQSEach consumer group / queue gets its own copy.
Need to replay history or add a new reader laterKafkaLog is kept; a new group can start from offset 0.
Strict order per entity (per user, per account)Kafka keyed by entity, or SQS FIFO with message group idOrder holds per partition / per group.
Very high throughput streams (clicks, logs, metrics)Kafka / KinesisSequential disk writes, batching, partitions spread load.
Complex routing by pattern or headerRabbitMQTopic and headers exchanges.
Fire-and-forget hint, loss is fineRedis Pub/SubFastest, nothing stored.
Small team, already on Redis, modest volumeRedis StreamsNo new system to run.
Per-message retry and parallelism beyond partition count, on KafkaKafka share groupsQueue semantics on a Kafka topic.
In everyday words
A to-do list gets crossed off and thrown away. A diary is kept, and anyone can read it again from page 1.
Actually
Kafka keeps records until retention (default 168 hours) or compaction removes them, whatever the readers do. SQS keeps a message only until a consumer deletes it after processing. That difference (kept log vs consumed message) is the root of the whole table.

Say "task → queue, event → log", then name the one property that decides it: replay, fan-out, order or per-message retry.

Source: Kafka intro · SQS queue types

13Transactional outbox and event-driven design

Writing to the database and the broker without losing either interview favourite

The dual-write problem: a service saves an order in its database, then publishes "order placed" to Kafka. If it crashes between the two, the database and the broker disagree. You cannot wrap both in one transaction.

Transactional outbox The service writes the order row and an outbox row in one database transaction; a relay reads new outbox rows and publishes them to Kafka. ONE TX PUBLISH Serviceorder API Database orders row outbox row Relaypoller or CDC Kafkaorders topic LEGEND Database Relay (publishes later) Broker
Both rows commit together or not at all. The relay may publish twice, so consumers must be idempotent.

The relay is either a polling publisher (a loop that reads unsent outbox rows) or CDC, change data capture: a tool like Debezium that reads the database's own change log (MySQL binlog, Postgres WAL). Page 10 covers both in detail: outbox and CDC.

Event-driven architecture: four patterns

PatternThe event carriesTrade-off
Event notification"Something changed", plus an idSmall events; readers call back for details, which re-couples them
Event-carried state transferThe full new dataReaders keep a local copy and never call back; bigger events, stale copies
Event sourcingEvery change, kept forever as the source of truthFull history and replay; harder queries and schema changes
CQRS(Not an event type) separate write and read models, often fed by eventsFast reads; eventual consistency between the models
In everyday words
Write the letter and drop it in your own outbox tray in the same move. The office boy carries the tray to the post office later. Nothing is half done.
Actually
microservices.io: the outbox service "first stores the message in the database as part of the transaction that updates the business entities". The relay can be "transaction log tailing" or a "polling publisher". Fowler (2017) names the four event-driven patterns above; for event notification, the source "doesn't expect direct responses".

"Save and publish" in one service = outbox. Never write to the DB and Kafka as two separate steps.

Source: microservices.io: Transactional outbox · Transaction log tailing · Fowler: What do you mean by "Event-Driven"? · Debezium outbox router

Practice

—Common mistakes and traps

What interviewers listen for traps

Trap
"Kafka guarantees order"
Only inside one partition. Say "per key, because the key picks the partition".
Trap
Adding consumers to fix lag
Past the partition count, extra consumers sit idle. Add partitions or speed up the handler.
Trap
"acks=all means no data loss"
Not with min.insync.replicas=1. Set RF 3, min ISR 2.
Trap
"Exactly-once" end to end
Kafka's EOS covers Kafka writes only. Side effects need idempotent consumers.
Trap
Adding partitions later
Changes hash % N. Existing keys move, so per-key order breaks across the change.
Trap
Infinite retry on one message
Blocks the partition forever. Cap retries, then DLQ.
Trap
Visibility timeout shorter than the job
The SQS message reappears mid-job; a second worker repeats it.
Trap
Dual write to DB and broker
A crash between them leaves them out of sync. Use the outbox.
Trap
Using Redis Pub/Sub for jobs
Nothing is stored. A restarting worker silently loses messages.

—Interview questions

20 questions, easy to design-level. Try first, then open the answer practice

1. Why would you put a queue between two services? easy
Show answer

Three reasons. Load levelling: the queue absorbs spikes, so the worker fleet is sized for average load, not peak. Decoupling: the sender does not need the receiver to be up, or even to know who it is. Fault isolation: if the receiver crashes, messages wait instead of requests failing. Cost: no immediate answer, eventual consistency, duplicates to handle, one more system to run. Example: an upload API that enqueues "resize image" jobs returns in milliseconds even if resizing takes 5 seconds. Follow-up: "what if the queue keeps growing?" → consumers are slower than producers on average; scale consumers or shed load.

2. Queue vs pub/sub: what is the difference? easy
Show answer

In a queue (point-to-point), each message is processed by one consumer; adding consumers splits the work (competing consumers). In pub/sub, each subscriber gets its own copy; adding subscribers adds readers, not workers. Use a queue for tasks ("send this email"), pub/sub for events ("order placed" for billing, email and analytics). Kafka does both: inside a consumer group it is a queue (one consumer per partition), across groups it is pub/sub. On AWS, SNS → several SQS queues gives the same "pub/sub of queues" shape.

3. What is a Kafka partition and why does it exist? easy
Show answer

A partition is an ordered, append-only log; a topic is split into several. Partitions exist for scale (they spread across brokers, so one topic can exceed one machine's disk and throughput) and parallelism (one consumer per partition per group). The cost is ordering: Kafka orders messages within a partition only. Each message has an offset, its position number. Choosing the count: estimate peak throughput ÷ per-partition throughput, and at least your peak consumer count, because you cannot have more active consumers in a group than partitions.

4. How does Kafka decide which partition a message goes to? easy
Show answer

If the message has a key, the default partitioner hashes it (murmur2) modulo the partition count, so the same key always lands in the same partition, which keeps per-key order. With no key, the producer uses a "sticky" strategy: fill a batch for one partition, then move on, to get big efficient batches. A custom partitioner is possible. Trap: adding partitions changes the modulo, so existing keys can move and per-key order breaks across that moment. Hot keys (one celebrity user) overload a single partition; fix with key salting or a different key.

5. I have 6 partitions and 10 consumers in one group. What happens? easy
Show answer

6 consumers get one partition each and 4 sit idle. Within a group, each partition is read by exactly one consumer, so the partition count caps parallelism. Idle consumers are not useless: they take over if an active one dies (after a rebalance). To use all 10, increase partitions to at least 10 (mind key remapping), or use a share group (KIP-932), where many consumers can read the same partition with per-record acks, at the cost of ordering.

6. What are at-most-once, at-least-once and exactly-once? medium
Show answer

They describe what can happen on a crash. At-most-once: commit the offset before processing; a crash loses the message. At-least-once: process, then commit; a crash causes a repeat. Exactly-once: the effect happens once. Kafka offers it inside Kafka via the idempotent producer + transactions (read-process-write with offsets in the same transaction). End to end with outside systems you get "effectively once" = at-least-once delivery + an idempotent consumer (dedup table keyed by message id, or upserts). Most brokers default to at-least-once.

7. How do you make a consumer idempotent? medium
Show answer

Options, best first. (1) Store the message id in a "processed" table with a unique key, in the same DB transaction as the work; a repeat fails the insert and is skipped. (2) Make the operation naturally idempotent: "set status = shipped" instead of "increment count". (3) Conditional writes with a version or sequence number: apply only if newer. (4) For external calls, pass an idempotency key (e.g. to a payment API). The id must be stable across retries, so it comes from the producer (event id), not from the broker's delivery. Keep dedup records at least as long as redelivery is possible.

8. Explain acks=0, 1 and all. medium
Show answer

acks=0: the producer does not wait; fastest, any failure loses data. acks=1: the leader writes it to its log and replies; if the leader dies before followers copy it, the write is lost. acks=all: the leader waits until every in-sync replica has it. On its own that is not enough, because the ISR can shrink to just the leader; pair it with min.insync.replicas=2 and replication factor 3, so a write needs 2 copies or is refused with NotEnoughReplicas. Since Kafka 3.0 the producer default is acks=all with idempotence on.

9. What is the ISR, and what is unclean leader election? medium
Show answer

The ISR (in-sync replica set) is the leader plus the followers that are fully caught up; a follower that falls behind for longer than replica.lag.time.max.ms (30 s default) is removed. Only ISR members may become leader, so acked writes survive. If every ISR member dies, Kafka can wait for one to come back (stay unavailable but consistent) or, with unclean.leader.election.enable=true, promote an out-of-sync replica (available again, but acked writes are lost). The default is false: Kafka picks consistency. That is a CAP choice in one config.

10. How does the idempotent producer avoid duplicates? medium
Show answer

The broker assigns the producer a producer id (PID). The producer numbers each batch per partition with a sequence number. The broker remembers the last sequence per PID and partition; a retried batch with a number it already has is acked but not written again, and a gap is an error. It needs acks=all and at most 5 in-flight requests per connection, which also keeps order on retry. Limit: it dedups only within one producer session; a restarted producer gets a new PID unless it uses a transactional.id.

11. What is log compaction and when would you use it? medium
Show answer

With cleanup.policy=compact, Kafka keeps at least the latest record for each key and removes older ones in the background. Offsets never change. A record with a null value (tombstone) deletes the key; tombstones stay for delete.retention.ms (1 day default) so readers see the delete. Use it for "current state" topics: user profiles, config, CDC changelogs, Kafka's own __consumer_offsets. A new service can rebuild a full table by reading from offset 0. Compare with time retention (default 168 hours), which drops old data regardless of key.

12. What causes rebalances and how do you reduce their pain? medium
Show answer

A rebalance happens when a consumer joins, leaves, crashes (missed heartbeats beyond session.timeout.ms) or takes longer than max.poll.interval.ms (5 min default) between polls. Old eager rebalances paused the whole group. Fixes: keep handlers fast or lower max.poll.records; use cooperative rebalancing (only moved partitions pause); use static membership (group.instance.id) so a quick restart does not trigger one; in Kafka 4.x use the new protocol (group.protocol=consumer, KIP-848), where the broker drives incremental assignment. Commit offsets before partitions are revoked to avoid repeats.

13. What is consumer lag and how do you act on it? medium
Show answer

Lag = log end offset minus the group's committed offset, per partition. It tells you how far behind readers are, in messages; convert to time by dividing by the arrival rate. Alert on lag that keeps growing, not on a fixed small number. Causes: slow handler, too few consumers, a hot partition, a stuck poison message, or rebalance storms. Actions: scale consumers up to the partition count, batch work, fix the slow dependency, move poison messages to a DLQ, or add partitions. Tools: kafka-consumer-groups.sh --describe, Burrow, or the cloud's metrics.

14. How do you handle a poison message in Kafka? medium
Show answer

A message that always fails blocks its partition, because order is per partition (head-of-line blocking). Retry a few times with backoff, then publish it to a dead-letter topic with error details and the original topic/partition/offset in headers, commit the offset and move on. Alert on DLQ size; replay after a fix. For transient failures use retry topics with delays (e.g. 1 min, 10 min) before the DLQ. Trade-off: once a message leaves the main line, later messages with the same key may be processed before it, so per-key order breaks; if order matters, pause that key instead.

15. SQS: what is the visibility timeout and what goes wrong with it? medium
Show answer

When a consumer receives a message, SQS hides it from others for the visibility timeout (default 30 s, max 12 h). The consumer must delete it before the timeout ends, or it becomes visible and another consumer gets it. If jobs take longer than the timeout, you get duplicate processing; fix by setting it above the worst job time or extending it with ChangeMessageVisibility as a heartbeat. After maxReceiveCount receives, a redrive policy moves the message to a DLQ. Use long polling (WaitTimeSeconds up to 20 s) to cut empty receives and cost.

16. SQS standard vs FIFO: when do you pick FIFO? medium
Show answer

Standard: at-least-once, best-effort order, nearly unlimited throughput. FIFO: strict order per message group id and exactly-once processing via a 5-minute deduplication window, but limited throughput: 300 API calls/s per action, 3,000 messages/s with batching (higher in high-throughput mode). Pick FIFO when order per entity matters (bank transactions per account), and use the account id as message group id so different accounts still run in parallel. Otherwise prefer standard plus idempotent consumers, which scales further and is cheaper.

17. Kafka vs RabbitMQ vs SQS: how do you choose? hard
Show answer

Use the "task vs event" rule. Tasks done once, with per-message retries and DLQ → SQS (zero operations on AWS) or RabbitMQ (rich routing with exchanges, push with prefetch, priorities). Events many teams consume, need for replay, very high throughput, per-key order → Kafka (kept log, partitions, consumer groups, compaction). Costs: Kafka has more to operate and partition planning; RabbitMQ deletes acked messages so no replay; SQS has no replay and FIFO throughput limits. A common combo: Kafka as the event backbone, SQS or RabbitMQ for job queues fed from it.

18. How do you publish an event when you save to the database, without losing either? hard
Show answer

Use the transactional outbox. In the same local transaction that writes the business row, insert a row into an outbox table. A relay then publishes outbox rows to the broker, either by polling the table or by CDC (Debezium tailing the MySQL binlog or Postgres WAL). If the transaction rolls back, no event exists; if the relay crashes, it resumes. The relay can publish twice, so consumers must be idempotent. Avoid "write DB, then publish" (crash in between loses the event) and "publish, then write DB" (event about something that never happened).

19. Design the notification system for an e-commerce site: order placed → email, SMS, push, analytics. hard
Show answer

The order service writes the order + an outbox row; a relay publishes OrderPlaced to a Kafka topic keyed by order id (or customer id for per-customer order). Separate consumer groups: email, SMS, push, analytics, each scaling up to the partition count (size for peak, e.g. 24 partitions). Each sender is idempotent via a dedup table on (event id, channel). Provider failures: retry with backoff, then a dead-letter topic plus alert; rate-limit per provider. Analytics can lag; email should not, so alert on email lag. Replay lets a new channel (WhatsApp) start from history.

20. What breaks if one Kafka key is extremely hot? How would you scale it? hard
Show answer

All messages for that key go to one partition, so one broker and one consumer carry it: lag grows on that partition while others sit quiet, and adding consumers does not help. Options: (1) salt the key (user42#0..7) to spread it across 8 partitions, then merge downstream, accepting loss of strict per-key order; (2) pre-aggregate in the producer (send counts every second, not every click); (3) give the hot entity its own topic; (4) if order is not needed, use a share group or no key. First, prove it with per-partition lag and bytes-in metrics.

—Cheat sheet

Numbers and facts to memorise (checked 2026-10-25) reference

FactValueSource
Kafka order guaranteePer partition onlyKafka intro
Readers per partition per groupExactly 1Kafka design
log.retention.hours default168 (7 days)Kafka broker configs
delete.retention.ms default (tombstones)86400000 ms (1 day)Kafka broker configs
Producer acks defaultall (since 3.0; was 1)Kafka producer configs
enable.idempotence defaulttrue (since 3.0)Kafka producer configs
min.insync.replicas default1Kafka broker configs
Durable recipeRF 3, min ISR 2, acks=allKafka design
max.poll.interval.ms default300000 ms (5 min)Kafka consumer configs
session.timeout.ms default45000 ms (classic protocol)Kafka consumer configs
New rebalance protocol (KIP-848)GA in Kafka 4.0Kafka 4.0 release notes
Share groups (KIP-932)Early access 4.0, preview 4.1, production-ready 4.2Kafka release notes
SQS visibility timeoutDefault 30 s, max 12 hSQS quotas
SQS retentionDefault 4 d, 60 s to 14 dSQS quotas
SQS max message size1 MiBSQS quotas
SQS long pollMax 20 sSQS polling
SQS FIFO throughput300 calls/s, 3,000 msg/s batchedSQS queue types
SQS FIFO dedup window5 minutesSQS FIFO docs
SQS maxReceiveCount1 to 1,000SQS DLQ docs
RabbitMQ prefetch advice100–300rabbitmq.com confirms
Redis Pub/SubAt-most-once, not storedredis.io
Google Pub/Sub ack deadlineDefault 10 s, max 600 sGoogle Cloud docs

—Sources

WhatWhere
Kafka concepts, design, configsKafka intro, Kafka documentation (4.x)
Kafka improvement proposalsKIP-98, KIP-848, KIP-932
Messaging patternsEnterprise Integration Patterns, Azure Architecture Center
Outbox, idempotent consumermicroservices.io, Debezium
Event-driven patterns, ESBFowler 2017, Lewis & Fowler 2014, IBM
SQS / SNS numbersSQS quotas, queue types, SNS to SQS
RabbitMQ, Redis, Google Pub/Subrabbitmq.com, redis.io, docs.cloud.google.com
Seen in our codellm_router/src/tc-monetization/iaam-consumer.ts, kafka-client.ts (via the earlier page message-brokers-explained.html)
Not provenmurmur2 is named only in Kafka source code (BuiltInPartitioner.java), not in the docs; "extra consumers sit idle" is not stated in the 4.x docs, it follows from "exactly one consumer" per partition; SQS high-throughput FIFO limit (two AWS pages disagree: 30,000 vs 70,000 TPS)