Contents
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.
A broker is a middleman program: the sender drops a message and moves on, and the receiver picks it up when it is ready.
- 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.
log.retention.hours), 7 days—Words you need
Why and what
01Why async messaging at all
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.
| Benefit | What it means | Example |
|---|---|---|
| Load levelling | The queue soaks up a spike. Workers drain it at a steady speed. | Black Friday orders pile up; 10 workers still process them. |
| Decoupling | Sender does not know who reads, or when. New readers need no sender change. | Add a fraud checker to "order placed" without touching checkout. |
| Fault isolation | A dead reader does not break the sender. Messages wait until it is back. | Email service is down for 10 minutes; no order fails. |
| Independent scaling | Add workers only where the work is. | Scale image resizers, not the upload API. |
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
Queue: one message, one worker
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
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
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.
In an interview, keep the pipe dumb. Put routing and rules in the services.
Kafka in depth
04Kafka: topic, partition, offset, key
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.
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
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
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.
| Setup | What happens |
|---|---|
| 6 partitions, 3 consumers in a group | Each consumer reads 2 partitions. |
| 6 partitions, 6 consumers | One each. Maximum parallelism. |
| 6 partitions, 8 consumers | 2 consumers idle. More readers does not help. |
| 2 groups ("billing", "email") on one topic | Both 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 stillclassic: 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.
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
| Policy | Keeps | Use it for | Key setting |
|---|---|---|---|
cleanup.policy=delete (default) | Everything, until it is older than the limit | Event streams: clicks, orders, logs | retention.ms / log.retention.hours = 168 |
cleanup.policy=compact | The newest record for each key, forever | Current state: user profile, config, a changelog for a table | delete.retention.ms = 86400000 ms, 1 day (how long tombstones stay) |
compact,delete | Newest per key, but also dropped when old | State that may expire | Both |
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
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.
acks | Producer waits for | Speed | Can lose data when |
|---|---|---|---|
0 | Nothing | Fastest | Any failure. No retry possible. |
1 | The leader's own write | Fast | Leader dies before a follower copies it. |
all (-1) | Every replica in the ISR | Slowest | Only 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:
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).
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
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.
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
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
A consumer does two things: the work, and saving its bookmark (commit the offset, or delete the message). Only their order decides the promise.
| Promise | Order | On a crash | Fine for |
|---|---|---|---|
| At-most-once | Bookmark, then work | Message lost | Metrics, live location pings |
| At-least-once | Work, then bookmark | Message repeated | Almost everything (the default) |
| Exactly-once (effective) | At-least-once + skip repeats | Repeat arrives, is ignored | Payments, 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.
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
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
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
| Model | Who controls speed | Examples | Overload protection |
|---|---|---|---|
| Pull | Consumer asks when ready | Kafka, SQS, Redis Streams | Natural: a slow reader just falls behind (lag) |
| Push | Broker sends as messages arrive | RabbitMQ (basic.consume), SNS, Google Pub/Sub push | Prefetch limit / flow control, or the reader drowns |
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
Amazon SQS: the managed queue
| SQS | Standard | FIFO |
|---|---|---|
| Delivery | At-least-once | Exactly-once processing (5-minute dedup window) |
| Order | Best effort | Strict, per message group id |
| Throughput | "Nearly unlimited" | 300 calls/s per action, 3,000 msg/s with batching (high-throughput mode: much more) |
| DLQ | Standard queue | Must 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:
WaitTimeSecondsup 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
order.*.eu. Pub/sub with filters.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 feature | Stored? | Promise | Use for |
|---|---|---|---|
Pub/Sub (PUBLISH / SUBSCRIBE) | No. Nobody listening = lost. | At-most-once | Live chat typing dots, cache-invalidation hints |
Streams (XADD / XREADGROUP) | Yes, a log | At-least-once with XACK; unacked entries sit in the pending list (XPENDING), and XAUTOCLAIM hands them to another reader | Small Kafka-like jobs when you already run Redis |
All of them, side by side
| System | Shape | Default promise | Kept after read? | Push / pull | Key numbers |
|---|---|---|---|---|---|
| SQS standard | Queue | At-least-once | No (consumer deletes) | Pull | Keep 4 d (max 14), hide 30 s (max 12 h) |
| SQS FIFO | Queue | Exactly-once processing | No | Pull | 300 / 3,000 per s |
| SNS | Pub/sub | Fan-out | No | Push | Filter policies |
| Kafka | Kept log | At-least-once; exactly-once inside Kafka | Yes | Pull | Retention 168 h default |
| RabbitMQ | Exchanges → queues | At-least-once with manual acks | No (classic/quorum), Yes (streams) | Push | Prefetch 100–300 |
| Redis Pub/Sub | Pub/sub | At-most-once | No | Push | Lost if no listener |
| Redis Streams | Kept log | At-least-once | Yes | Pull | Pending entries list |
| Google Pub/Sub | Pub/sub | At-least-once, no order by default | Until acked | Both | Ack deadline 10 s (max 600), keep 7 d (max 31) |
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 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.
| Situation | Pick | Why |
|---|---|---|
| Background jobs, one worker each, no replay | SQS / RabbitMQ | Per-message ack, retries, DLQ built in. Scale workers freely. |
| Many teams react to the same event | Kafka, or SNS + SQS | Each consumer group / queue gets its own copy. |
| Need to replay history or add a new reader later | Kafka | Log 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 id | Order holds per partition / per group. |
| Very high throughput streams (clicks, logs, metrics) | Kafka / Kinesis | Sequential disk writes, batching, partitions spread load. |
| Complex routing by pattern or header | RabbitMQ | Topic and headers exchanges. |
| Fire-and-forget hint, loss is fine | Redis Pub/Sub | Fastest, nothing stored. |
| Small team, already on Redis, modest volume | Redis Streams | No new system to run. |
| Per-message retry and parallelism beyond partition count, on Kafka | Kafka share groups | Queue semantics on a Kafka topic. |
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
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.
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
| Pattern | The event carries | Trade-off |
|---|---|---|
| Event notification | "Something changed", plus an id | Small events; readers call back for details, which re-couples them |
| Event-carried state transfer | The full new data | Readers keep a local copy and never call back; bigger events, stale copies |
| Event sourcing | Every change, kept forever as the source of truth | Full history and replay; harder queries and schema changes |
| CQRS | (Not an event type) separate write and read models, often fed by events | Fast reads; eventual consistency between the models |
"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
min.insync.replicas=1. Set RF 3, min ISR 2.hash % N. Existing keys move, so per-key order breaks across the change.—Interview questions
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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
| Fact | Value | Source |
|---|---|---|
| Kafka order guarantee | Per partition only | Kafka intro |
| Readers per partition per group | Exactly 1 | Kafka design |
log.retention.hours default | 168 (7 days) | Kafka broker configs |
delete.retention.ms default (tombstones) | 86400000 ms (1 day) | Kafka broker configs |
Producer acks default | all (since 3.0; was 1) | Kafka producer configs |
enable.idempotence default | true (since 3.0) | Kafka producer configs |
min.insync.replicas default | 1 | Kafka broker configs |
| Durable recipe | RF 3, min ISR 2, acks=all | Kafka design |
max.poll.interval.ms default | 300000 ms (5 min) | Kafka consumer configs |
session.timeout.ms default | 45000 ms (classic protocol) | Kafka consumer configs |
| New rebalance protocol (KIP-848) | GA in Kafka 4.0 | Kafka 4.0 release notes |
| Share groups (KIP-932) | Early access 4.0, preview 4.1, production-ready 4.2 | Kafka release notes |
| SQS visibility timeout | Default 30 s, max 12 h | SQS quotas |
| SQS retention | Default 4 d, 60 s to 14 d | SQS quotas |
| SQS max message size | 1 MiB | SQS quotas |
| SQS long poll | Max 20 s | SQS polling |
| SQS FIFO throughput | 300 calls/s, 3,000 msg/s batched | SQS queue types |
| SQS FIFO dedup window | 5 minutes | SQS FIFO docs |
SQS maxReceiveCount | 1 to 1,000 | SQS DLQ docs |
| RabbitMQ prefetch advice | 100–300 | rabbitmq.com confirms |
| Redis Pub/Sub | At-most-once, not stored | redis.io |
| Google Pub/Sub ack deadline | Default 10 s, max 600 s | Google Cloud docs |
—Sources
| What | Where |
|---|---|
| Kafka concepts, design, configs | Kafka intro, Kafka documentation (4.x) |
| Kafka improvement proposals | KIP-98, KIP-848, KIP-932 |
| Messaging patterns | Enterprise Integration Patterns, Azure Architecture Center |
| Outbox, idempotent consumer | microservices.io, Debezium |
| Event-driven patterns, ESB | Fowler 2017, Lewis & Fowler 2014, IBM |
| SQS / SNS numbers | SQS quotas, queue types, SNS to SQS |
| RabbitMQ, Redis, Google Pub/Sub | rabbitmq.com, redis.io, docs.cloud.google.com |
| Seen in our code | llm_router/src/tc-monetization/iaam-consumer.ts, kafka-client.ts (via the earlier page message-brokers-explained.html) |
| Not proven | murmur2 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) |