Design a Distributed Message Queue
This page is one interview loop in three rounds. All three rounds design the same system. Each round opens with the interviewer raising the scope, and the design from the round before has to evolve to meet it.
| Round 1: Mid-level | Round 2: Senior | Round 3: Architect | |
|---|---|---|---|
| Story | A task queue between one team's order service and its fulfillment workers | Every team in the company publishes and subscribes | Four regions, regulated tenants, long retention |
| Level (Amazon) | SDE II (L5) | Senior SDE (L6) | Principal (L7) |
| Traffic | 50M messages/day, ~2K/s peak | 2B/day, ~81K/s peak | 20B/day, ~810K/s peak across 4 regions |
| Keeps data | Until acknowledged, at most 4 days | 7 days, replayable | 7 to 90 days by policy, cold data in S3 |
| Survives | Losing a broker | Losing an AZ | Losing a region |
| Availability | 99.9% | 99.99% | 99.999% per region |
| Reading time | ~35 min | ~40 min | ~45 min |
You can start at any round. Rounds 2 and 3 open with a "Where we left off" summary that catches you up.
Loop Opener: What Is a Message Queue?
You Already Use One: a To-Do List Between Two People
One person writes tasks on a shared list. Another person picks one up whenever they are free, does it, and crosses it off. The writer never waits for the reader, and if twenty tasks arrive at once, they simply sit on the list until someone gets to them.
Services do the same thing. The order service writes "ship order 42" and moves on. A fulfillment worker picks it up a moment later, or an hour later if it is busy.
| Writer (producer) | Message | Reader (consumer) |
|---|---|---|
| Order service | "ship order 42" | Fulfillment worker |
| Payment service | "payment 9 settled" | Ledger, email, fraud check |
| Upload service | "resize image 7" | Thumbnail worker |
| Every service | "user 5 clicked X" | Analytics |
A message queue is that list, run as a service. What it buys us:
- The producer doesn't wait. Sending a message takes milliseconds, even if the work takes minutes.
- Bursts don't crush the consumer. The queue absorbs a spike, and consumers work through it at their own pace.
- Either side can fail without taking the other down. If the workers crash, messages wait for them.
What Changes When the List Is Distributed
On one machine, the list is easy. Once it spans many machines, three things get hard:
- The list must survive machines dying. A message the queue said "got it" to must not vanish when a disk or a server does.
- Two workers must not both take the same task at the same time, at least not on purpose.
- A task a worker dropped must come back. If a worker takes a message and crashes, somebody else has to get it.
The Question the Whole Loop Answers
How do we make sure every message is delivered, none are lost, and order holds where we promised it, while machines and consumers fail?
The answer gets sharper every round:
- Round 1: a replicated, partitioned queue. Messages are leased to one worker at a time and come back if the worker dies.
- Round 2: order per key, no duplicates from retries, and many teams reading the same messages, which turns the queue into a log.
- Round 3: regions, laws and money, and whether we should run this at all.
Round 1 · Mid-level · "A Task Queue for One Team's Order Pipeline"
~35 min · SDE II (L5) · 1 region, 3 AZs · ~2K msg/s peak · 99.9%
R1.1 Establish Design Scope
The interviewer says: "Design a distributed message queue." Before we draw anything, we ask questions, and we say out loud what each answer changes.
| We ask | Interviewer answers | What it changes in the design |
|---|---|---|
| Who uses it? | One team. The order service sends "fulfil this order" tasks, and a pool of fulfillment workers does them. | One producer service, one consumer team. A queue: each message is done by one worker, then removed. |
| How big is a message, and how many? | About 2 KB on average, never more than 256 KB. About 50 million a day. | Small messages, moderate volume. We cap messages at 256 KB; anything bigger goes to S3 and the message carries a pointer. (For comparison, Amazon SQS raised its own limit from 256 KiB to 1 MiB in 2025.) |
| How many producers and consumers? | A few dozen order-service instances, and 30 to 100 workers depending on load. | Many workers read the same queue at once, so the queue must hand each message to only one of them at a time. |
| Must messages be processed in order? | No. Orders are independent. | We can spread messages over many machines freely. This is what makes scaling easy this round. |
| Can a message be processed twice? | Yes, if that's rare. The workers are idempotent: processing the same message twice has the same effect as processing it once (they check whether the order was already shipped). | We can promise at-least-once delivery: every message is delivered, sometimes more than once. That is far cheaper than promising exactly once. |
| How long do we keep a message nobody has processed? | If the workers are down, keep messages for up to 4 days. | Storage is sized for 4 days of messages in the worst case, and messages older than 4 days expire. |
| Must it survive losing a machine? | Yes. An acknowledged message must never be lost. | We must store every message on more than one machine before we tell the producer "got it". |
Out of scope for this round:
- Ordering. Nobody needs it yet.
- Several teams reading the same messages. One team, one queue.
- Replaying old messages. A processed message is gone.
- Other regions. One AWS region, three Availability Zones (AZs).
The interviewer will widen this scope later. Write your out-of-scope list where you can see it: in a multi-round loop, some of it comes back.
The agreement that makes the whole design possible is in two rows of that table: at-least-once delivery, plus idempotent consumers. Everything below leans on it.
R1.2 Functional Requirements, Derived Step by Step
We read the problem one phrase at a time and turn each phrase into a requirement:
| Phrase from the problem | Requirement |
|---|---|
| "The order service hands work off" | send: store a message and return an ID |
| "A worker picks it up" | receive: give a waiting worker one or more messages |
| "Done" | delete: the worker says it finished, and the message is removed |
| "The worker died in the middle of a task" | A message that was received but never deleted must come back for another worker |
| "A task that always fails" | After a number of attempts, stop retrying it and set it aside for a human |
Not yet: ordering, removing duplicates, delaying a message, many teams reading one message, replay. The interviewer may bring these back.
R1.3 Non-Functional Requirements: the Questions
Numbers come in R1.7. For now, the qualities and why each matters:
- Availability. The producer must never be blocked. If the queue can't take a message, the order service can't hand off work, and customers wait.
- Durability. Once we say "got it" to the producer, the message must survive any single machine failing, including its disk.
- Latency. Close to real time: a waiting worker should see a message within a fraction of a second of it being sent.
- Scalability. If traffic grows, we add brokers (the servers that store and hand out messages) and get more throughput.
- Delivery. At-least-once, as agreed.
Why not exactly-once? Picture the last step of a task: the worker ships the order, then calls delete. If the worker crashes between the two, the queue can't tell whether the order shipped, so it must hand the message out again, or risk losing it. If instead the worker deletes first and then ships, a crash between the two loses the task. No choice of order inside the queue closes this gap, because the queue and the worker's database are two different systems. "Exactly once" only happens when the worker makes its own effects idempotent, which ours already are. The queue's job is "at least once, and duplicates are rare".
Delivery semantics are set by when each side acknowledges, not by the queue alone:
| Semantics | Producer | Consumer | Result | Fits |
|---|---|---|---|---|
| At-most-once | Sends once, never retries | Deletes (or commits) before processing | Can lose messages; never duplicates | Metrics samples, logs where a gap is fine |
| At-least-once (ours) | Retries until the queue confirms the message is stored | Deletes after processing | Never loses; a crash between "done" and "delete" redelivers | Orders, payments, notifications, with idempotent consumers |
| Exactly-once effect | Retries, and the queue drops the retried copies (Round 2) | Records "done" in the same transaction as the work itself | Each effect happens once, at the cost of more moving parts | Ledgers, inventory counts |
R1.4 The API
Three calls, internal only, over HTTPS.
Send
httpPOST /v1/queues/order-fulfillment/messages HTTP/1.1 Host: mq.internal Content-Type: application/json { "body": "{\"orderId\": \"42\", \"action\": \"ship\"}", "attributes": { "source": "order-service" } }
httpHTTP/1.1 200 OK Content-Type: application/json { "messageId": "m-3-18442" }
A 200 means the message is stored on a majority of its replicas (step 1.3). Before that, the answer never comes.
Receive
httpPOST /v1/queues/order-fulfillment/messages:receive HTTP/1.1 Host: mq.internal Content-Type: application/json { "maxMessages": 10, "waitSeconds": 20, "visibilitySeconds": 30 }
httpHTTP/1.1 200 OK Content-Type: application/json { "messages": [ { "messageId": "m-3-18442", "receiptHandle": "rh.3.18442.1.Zk9wQ2xhVGhlU2ln", "body": "{\"orderId\": \"42\", \"action\": \"ship\"}", "receiveCount": 1, "sentAt": "2026-09-26T12:00:00.000Z" } ] }
waitSeconds(0 to 20) is how long the broker may hold the request open if there's nothing to return yet (step 1.5).visibilitySecondsis how long this message stays hidden from other workers while this worker processes it (step 1.2).- The receipt handle identifies this particular receive of the message, not just the message. If the message is received again later, it gets a new handle. Round 1's broker only uses the handle to find the message; Round 2 makes it enforce the lease.
Delete (acknowledge)
httpDELETE /v1/queues/order-fulfillment/messages/rh.3.18442.1.Zk9wQ2xhVGhlU2ln HTTP/1.1 Host: mq.internal
httpHTTP/1.1 204 No Content
| Status | When |
|---|---|
200 OK | Send stored the message; receive returned zero or more messages |
204 No Content | Delete succeeded, or the message was already deleted (deleting twice is harmless) |
400 Bad Request | A malformed request or handle |
403 Forbidden | The caller may not use this queue |
404 Not Found | No such queue |
413 Content Too Large | The body is over 256 KB |
429 Too Many Requests | The caller is over its rate limit (with Retry-After) |
503 Service Unavailable | The partition's leader is changing (step 1.3); retry with backoff |
Recap
- Three calls: send, receive with a wait and a visibility time, delete.
- At-least-once delivery; workers are idempotent.
- About 50 million messages a day, around 2 KB each, never more than 256 KB.
- Unprocessed messages kept up to 4 days.
- A "got it" means the message survives losing any one machine.
Let's build it, starting with the simplest thing that works.
R1.5 Design Evolution: From an In-Memory List to a Replicated Queue
Every step below follows the same pattern: a problem, your turn to think, the answer, and what the answer costs us. The cost is always the next problem.
Step 1.0: The Baseline
One server holding a list in memory. send appends to the list. receive removes the first item and returns it.
Synthesizing vector architecture diagram...
What's good about it: it's fast and simple, and for a prototype it works.
What it costs us: everything. A restart loses every message. A worker that crashes loses the message it took. One machine is all the capacity there is.
Step 1.1: The Broker Restarted and the Messages Are Gone
The problem: the broker is redeployed. Every message that was waiting in memory, about 20,000 of them, is gone. The order service had been told "got it" for every one. What would you do? How does the broker keep messages across a restart, without slowing every send to a crawl?
The log for one queue, on disk:
textqueue order-fulfillment, log 3 00000000000000000000.log ← segment: messages 0 .. 18,399 00000000000000000000.index ← every ~4 KB: offset → byte position 00000000000000018400.log ← active segment: new records are appended here 00000000000000018400.index one record in a segment: ┌────────┬───────┬───────────┬──────────┬────────────┬──────────────┬─────────┐ │ length │ CRC32 │ type │ offset │ timestamp │ attributes │ body │ │ 4 B │ 4 B │ msg / ack │ 8 B │ 8 B │ variable │ ≤256 KB │ └────────┴───────┴───────────┴──────────┴────────────┴──────────────┴─────────┘
The CRC32 is a checksum of the record, so a half-written record at the end of the file after a crash is detected and discarded. An ack record has no body; it names the offset it acknowledges.
Primitive: Write-Ahead Log & LSM-Trees
Step 1.2: A Worker Took a Message and Crashed
The problem: a worker receives "ship order 42", starts working, and its process is killed by a bad deploy. Order 42 never ships, and nobody knows. What would you do? How does the queue make sure a message taken by a dead worker gets done?
Synthesizing vector architecture diagram...
A message is only ever in one of these states. The only way out of "in flight" without a delete is back to "visible", which is the whole point.
Primitive: Distributed Locks & Leases
Step 1.3: The Broker's Disk Died
The problem: the broker's disk fails. Everything fsynced to it, including 20,000 messages we said "got it" to, is unreadable. What would you do?
Synthesizing vector architecture diagram...
The leader answers after the fastest follower. A slow or dead third replica doesn't slow the send.
Go deeper: Raft versus Kafka's ISR. Kafka replicates differently, and it's worth being able to say how. Kafka's leader keeps a list of in-sync replicas (ISR): followers that have kept up recently. With acks=all, the leader answers only after every replica in the ISR has the message, and min.insync.replicas=2 refuses writes if fewer than two are in sync. A follower that falls behind (by default, for 30 s) is dropped from the ISR instead of slowing everyone down. Kafka uses Raft too, but only for its cluster metadata (KRaft), not for the messages. Both designs survive losing one of three replicas without losing acknowledged data; they differ in how they treat a slow replica and how a leader is chosen. We use Raft for every log, as some Kafka-compatible brokers such as Redpanda do, because the majority rule gives one clear answer to "when is a message safe?" and one clear rule for elections.
Primitive: Distributed Consensus: Raft & Paxos · Drill: Raft consensus split brain
Step 1.4: One Broker Can't Take All the Traffic
The problem: every message for the queue goes through one log, so through one leader. At peak, that leader's CPU and disk are the limit for the whole queue, while the other brokers mostly wait. What would you do?
Primitive: Database Sharding & Partition Keys
Step 1.5: Workers Poll Constantly and Mostly Get Nothing
The problem: at night, the queue is nearly empty. Sixty idle workers ask each of three brokers "anything for me?" every 100 ms. That is requests a second, nearly all of them answered "no". What would you do?
Step 1.6: One Bad Message Crashes Every Worker, Forever
The problem: one message has a malformed address that makes the worker's parser throw. Each worker that receives it crashes, its lease expires, and the next worker receives it and crashes too. For three hours it keeps coming back and taking workers down with it. What would you do?
Drill: Message queue order pipeline
Round 1 Step Summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 1.0 | (baseline) | One broker, a list in memory | Loses everything on restart |
| 1.1 | Restart loses messages | Append-only segment log, fsync with group commit, ack records | A disk wait per send; still one machine |
| 1.2 | Worker crashes mid-task | Visibility timeout (lease), extend, at-least-once | Duplicates when work outlasts the lease |
| 1.3 | A disk dies | Raft per log: 3 replicas in 3 AZs, commit on 2 of 3 | Cross-AZ wait per send; 1–2 s failover |
| 1.4 | One leader is the limit | 6 partitions, leaders spread over brokers | No order across partitions |
| 1.5 | Empty polling | Long polling (≤ 20 s), pull | Open connections |
| 1.6 | Poison message | maxReceiveCount = 5 → DLQ, alarm, redrive | An unwatched DLQ is a graveyard |
Two costs are still open: there's no ordering, and only one team can read a queue, since a message is gone once one worker deletes it. Round 2 starts there.
R1.6 Architecture v1
Now the concepts get AWS names.
Synthesizing vector architecture diagram...
Follow a message from the top: the order service sends through the load balancer to whichever broker it lands on, and that broker appends it to a partition it leads and replicates it to the other two AZs. Workers skip the load balancer: they learn the brokers' addresses from the metadata store once, then hold a long poll open to each broker. The metadata store is off the path of every send and receive.
The pieces:
- Brokers: three EC2
m7g.largeinstances (2 Graviton vCPUs, 8 GiB), one per AZ. Each stores the segment logs of all 6 partitions on a 1 TB gp3 EBS volume, leads 2 partitions and follows 4. - Internal Network Load Balancer (NLB): a layer-4 load balancer that passes TCP connections through. It spreads producers' connections across brokers and health-checks them.
- Metadata store: three small instances (
m7g.medium) running one Raft group. It records the queues, their settings (visibility timeout,maxReceiveCount, retention), which brokers hold each partition, and the current leaders. - Client library: used by producers and workers. It retries failed sends with backoff, keeps one connection per broker for receives, and refreshes broker addresses from the metadata store when a broker stops answering.
- CloudWatch: metrics and alarms, including backlog depth, age of the oldest message and DLQ depth.
Trace 1: a send.
Synthesizing vector architecture diagram...
The producer waits for one cross-AZ round trip and two fsyncs (the leader's and the first follower's), which run at about the same time.
Trace 2: a receive, a lease, and a delete.
Synthesizing vector architecture diagram...
Trace 3: the worker crashes. The worker receives m-1-90211 at 12:00:00 with a 30-second lease and is killed at 12:00:05. Nothing happens until 12:00:30, when broker 2's lease deadline passes and the message becomes visible again. The next receive, from any worker, gets it with receiveCount = 2 and a new receipt handle. Order 42 ships once the second worker finishes; if the first worker had in fact shipped it before dying, the second worker sees that and does nothing, because workers are idempotent.
R1.7 Numbers
Traffic. A day has 86,400 seconds. We plan the peak at 3.5 times the average, the same ratio Round 2 uses.
| Quantity | Math | Value |
|---|---|---|
| Messages per day | given | 50,000,000 |
| Average rate | 50,000,000 ÷ 86,400 s | ≈ 579 msg/s |
| Peak rate | 579 × 3.5 | ≈ 2,025 msg/s (the "~2K" in the badge) |
| Bytes in at peak | 2,025 × 2 KB | ≈ 4.05 MB/s |
| Bytes out at peak | one consumer team, so about the same as in | ≈ 4.05 MB/s |
| Replication out of leaders at peak | 2 followers × 4.05 MB/s | ≈ 8.1 MB/s |
| Bytes per day | 50,000,000 × 2 KB | 100 GB/day |
(We use decimal units throughout: 1 KB = 1,000 bytes, 1 GB = 10⁹ bytes.)
Storage, worst case. Normally workers keep up, and the backlog is minutes of messages. We size for the case we promised to survive: workers down for the full 4 days.
The 1.2 covers record headers, ack records, indexes and free space. Each broker holds a third: 480 GB, on a 1 TB volume, so even the worst case leaves the disk under half full.
Partitions. How much one partition can take is a design choice, not a law. We set a planning ceiling of 1,000 msg/s per partition, low on purpose, so each partition's fsync rate and lease bookkeeping stay small.
Six partitions over three brokers: each leads two.
Disk operations. Each log fsyncs at most once per 2 ms group-commit window, so at most 500 times a second. A broker holds 6 logs, so at most fsyncs a second, plus the data writes themselves. gp3 gives 3,000 IOPS (disk operations per second) as a baseline, so we provision 6,000. At our real peak, each log sees about messages a second, so the true fsync rate is lower.
Brokers. Three is the minimum for 3 replicas in 3 AZs, and it's far more than the load needs: 4 MB/s in is a few percent of one m7g.large. After losing a broker, the other two lead all 6 partitions without strain.
Availability budget. 99.9% of a 30.4-day month is minutes of downtime. A broker failure costs its partitions 1 to 2 seconds of failed sends, which producers retry, so the budget is really for mistakes: a bad deploy, a misconfigured queue.
Latency budget: P99 from send to a waiting worker under 100 ms.
| Piece | Estimate |
|---|---|
| Producer to NLB to broker, same region | ≤ 1 ms |
| Waiting for the group-commit window | ≤ 2 ms |
| Leader's fsync, in parallel with sending to followers | 1–3 ms |
| To a follower in another AZ, its fsync, and back | 2–5 ms |
| Committed message handed to a waiting long poll | ≤ 1 ms |
| Total (critical path: 1 + 2 + max(leader, follower) + 1) | ≈ 5–9 ms |
The leader's fsync and the follower round trip run at the same time, so only the slower of the two counts. These are estimates, which we confirm with a load test before launch. They leave about 11 times headroom under the 100 ms target. The target is for a waiting worker: if every worker is busy, a message waits in the backlog, and that is queueing, not latency we can fix in the broker.
Monthly cost (us-east-1 on-demand prices, 730 hours a month, 3,040 GB of messages a month):
| Item | Math | Monthly |
|---|---|---|
3 × m7g.large brokers | 3 × $0.0816/h × 730 h | ≈ $179 |
| 3 × 1 TB gp3 | 3 × 1,000 GB × $0.08, plus 3 × 3,000 extra IOPS × $0.005 | ≈ $285 |
3 × m7g.medium metadata nodes | 3 × $0.0408/h × 730 h | ≈ $89 |
| NLB | $0.0225/h × 730 h, plus ~4.2 GB/h of sends ≈ 4.2 capacity units × $0.006/h × 730 h | ≈ $35 |
| Cross-AZ: replication | 2 copies × 3,040 GB × $0.02/GB ($0.01 each direction) | ≈ $122 |
| Cross-AZ: producers and workers | about 2/3 of each side's traffic crosses an AZ: 2 × (2/3 × 3,040 GB) × $0.02 | ≈ $81 |
| CloudWatch metrics and alarms | ≈ $20 | |
| Total | ≈ $810/month |
The same workload on Amazon SQS. SQS charges per request: $0.40 per million for standard queues, and each 64 KB of payload counts as one request. Every message needs a send, a receive and a delete.
| How we call it | Requests per month | Monthly |
|---|---|---|
| One message per call | 50M × 3 × 30.4 ≈ 4.56 billion | ≈ $1,820 |
| Batches of 10 on every call (20 KB, still one 64 KB unit) | ≈ 456 million | ≈ $180 |
Long polling's empty receives add a few dollars at most: 60 idle workers each waiting 20 seconds make about 7.9 million requests a month even if the queue were empty all month.
Say it plainly in the interview: at this scale, the broker fleet costs about the same as SQS, and SQS comes with no brokers to patch, no disks to watch and no on-call rotation. The fleet bill isn't the real cost; the people who run it are.
R1.8 Trade-Offs
| Choice | We chose | What we give up |
|---|---|---|
| Build vs Amazon SQS | In a real company at this scale: SQS. In the interview, we design the broker to show we understand what SQS does inside. | Building: a team on call for a system SQS already runs, for about the same bill. Buying: SQS's limits and API are fixed (1 MiB messages, 12-hour maximum visibility timeout, 14-day maximum retention). |
| At-least-once vs at-most-once | At-least-once: delete only after the work is done | Occasional duplicates, so every consumer must be idempotent. At-most-once (delete on receive) never duplicates but loses messages on every crash. |
| Replica count and quorum | 3 replicas, commit on 2 | Survives one failure. 5 replicas committing on 3 would survive two, but costs 5/3 of the storage, more cross-AZ traffic, and waits for the second-fastest of four followers instead of the fastest of two. |
| fsync before answering | Yes, with group commit | A few milliseconds per send. Without it, a simultaneous power loss in two AZs could lose acknowledged messages. |
| Long vs short polling | Long polling, up to 20 s | An open connection per waiting worker per broker. Short polling needs no held connections but wastes requests and adds up to a polling interval of latency. |
| Pull vs push | Pull | Push can shave a little latency, but the broker would need flow control to avoid flooding slow workers. |
R1.9 Failure Modes
| Failure | What you'd see | How the design responds |
|---|---|---|
| A broker crashes | Its two partitions get 503 on sends for 1 to 2 seconds; NLB health checks fail; workers' connections to it reset. | Followers elect new leaders with every committed message (step 1.3); producers retry; workers keep polling the other two brokers. Leases lived only in the old leader's memory, so the new leader treats every message not yet acknowledged as visible: messages that were in flight on that broker are delivered again. At peak, with about 2 seconds of processing per message (an assumption), that's roughly duplicates, which idempotent workers absorb. Receive counts restart too, so a poison message may get a few extra tries before the DLQ. |
| A worker is slower than its visibility timeout | The same message is processed by two workers; a rising count of receives with receiveCount > 1. | Workers with long jobs extend their lease before it ends. We alarm when many receives have a count above 1, which usually means the timeout is too short for the real processing time. |
| A disk fills up | Disk-usage alarm; if it reaches the limit, sends to that broker's partitions fail with 503. | A broker refuses new sends rather than drop stored ones. We alarm at 70% full. Retention (4 days) is the backstop, and the worst case uses 48% of the disk. The real fix is almost always the consumers: a full disk means nobody is processing. |
| A producer retry creates a duplicate | The same order appears twice in the queue. The send succeeded, but the 200 was lost on the way back, so the producer sent again. | Not handled by the queue this round; the idempotent workers make it harmless. This is Round 2's problem. |
| Two of three replicas of a partition are down | That partition refuses sends. | By design: without a majority, a send can't be made safe. The client library sends to other partitions instead, since order doesn't matter this round. |
R1.10 Pillar Check
| Pillar | What Round 1 covers |
|---|---|
| Reliability | Every log replicated to 3 AZs, committed on a majority; Raft elections pick a leader with every committed message; visibility-timeout leases return messages from dead workers; DLQ for poison messages REL 10 · REL 11 · REL 5 |
| Performance Efficiency | Sequential appends with group commit; long polling delivers to a waiting worker within milliseconds with almost no empty requests PERF 1 · PERF 3 |
| Security | Per-queue permissions (which service may send, which may receive: the equivalent of an IAM policy per queue); TLS for every connection; EBS volumes encrypted with KMS keys SEC 3 · SEC 8 · SEC 9 |
| Cost Optimization | About $810/month, derived; the honest comparison with SQS at $180–$1,820/month, and why the people cost decides it COST 5 · COST 11 |
| Operational Excellence | Light this round: alarms on backlog depth, age of the oldest message, and DLQ depth above zero OPS 8 |
| Sustainability | Light this round: Graviton brokers; long polling cuts idle requests from 1,800 to 9 a second SUS 5 |
R1.11 Round 1 Rubric and Follow-Ups
What a strong mid-level (L5) answer shows
- Asks whether order matters and whether duplicates are acceptable, and explains that "at-least-once plus idempotent consumers" is the agreement the design rests on.
- Explains why a queue doesn't delete on receive, and how a visibility timeout brings back a dead worker's message.
- Stores messages in an append-only log, fsyncs before answering, and replicates to a majority across AZs before saying "got it".
- Names the replication protocol and says how a new leader is chosen without losing committed messages.
- Uses long polling and knows why pull protects slow consumers.
- Handles poison messages with a receive count and a DLQ, and puts an alarm on the DLQ.
- Does the numbers, and says honestly that SQS is the right answer at this size.
Follow-up questions
-
"Why not just use a Postgres table as the queue?" Answer: at 2,000 messages a second, it can work: workers take rows with
SELECT ... FOR UPDATE SKIP LOCKED, which lets each worker lock a different row without waiting on the others. The costs show up as volume grows: every message is an insert, an update and a delete, so the table churns constantly and the database spends a lot of effort cleaning up dead rows; the queue shares a database with everything else; and it doesn't grow past one primary. It's a fair choice for a small system that already has Postgres. A log is built for exactly this workload. -
"Processing takes 8 seconds at P99 and up to 60 seconds for rare big orders. What visibility timeout do you set?" Answer: not the average, and not 60 seconds for everyone. We set 30 seconds, comfortably above the P99, and have the worker extend its lease every 10 seconds while it's still working. A big order keeps its lease as long as its worker is alive, and a crashed worker's message comes back within 30 seconds instead of 60.
-
"A broker is cut off from the other two for a minute, but it still thinks it's the leader. What stops it from accepting sends and losing them?" Answer: Raft's majority rule. The cut-off leader can append messages to its own log, but it can't get a second replica to store them, so nothing commits and it never answers
200. Its producers time out and retry through other brokers. Meanwhile the other two replicas elect a new leader in a higher term (Raft's election number). When the old leader reconnects, it sees the higher term, steps down, and throws away its uncommitted entries. No acknowledged message was ever on it alone.
Interview gotchas from this round's wrong answers
| Gotcha | Why it's wrong |
|---|---|
| "Delete the message when the worker receives it" | Any crash between receive and done loses the task. That's at-most-once. |
| "Retry forever" | A poison message never succeeds; it burns a worker on every attempt. Count receives and move it to a DLQ. |
| "Exactly-once is automatic" | The queue and the worker's database are separate systems; a crash between "work done" and "delete" forces a choice between duplicate and loss. Exactly-once needs idempotent consumers. |
| "Backups keep it durable" | Messages live minutes; last night's backup has none of today's. Durability comes from replicating before answering. |
| "Push is faster, so push" | Push floods slow consumers unless the broker adds flow control. Long-polling pull gets nearly the same latency. |
Round 2 · Senior · "Every Team Publishes and Subscribes"
~40 min · Senior SDE (L6) · 1 region, 3 AZs · ~81K msg/s peak · 99.99% · order per key, no duplicates from retries, replay for 7 days
R2.0 Where We Left Off
This is what the candidate says aloud in the first 60 seconds of Round 2. If you're starting here, it's everything you need from Round 1.
Round 1 in 60 seconds. "We built a task queue for one team: the order service hands 'ship this order' tasks to fulfillment workers. About 50 million messages a day, 2,000 a second at peak, 2 KB on average, one region, three AZs, 99.9%. Delivery is at-least-once and workers are idempotent. Each queue is split into 6 partitions. Each partition is an append-only segment log, fsynced with group commit, and replicated with Raft to 3 brokers in 3 AZs; a send is acknowledged once 2 of 3 replicas have it. A receive leases a message with a visibility timeout, 30 seconds by default: it's hidden until the worker deletes it, and it comes back if the worker dies. Deletes are ack records in the log. Workers long-poll for up to 20 seconds. After 5 failed receives, a message goes to a dead-letter queue with an alarm. Three
m7g.largebrokers, about $810 a month, and we said honestly that SQS would be the right buy at this size. Three costs are still open: there's no ordering, only one team can read a queue, and producer retries create duplicates."
Architecture v1, compact
textorder service ──► internal NLB ──► 3 brokers (m7g.large), one per AZ 6 partitions, each a Raft group of 3; commit on 2 of 3 segment log + ack records on 1 TB gp3, fsync (group commit) leases in the leader's memory: visibility 30 s, extend workers ──► long-poll every broker (≤ 20 s) ──► receive, delete receive count > 5 ──► DLQ + alarm metadata store: 3-node Raft group, off the path
Round 1 step summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 1.1 | Restart loses messages | Segment log, fsync, group commit, ack records | Disk wait per send |
| 1.2 | Worker crashes mid-task | Visibility-timeout lease | Duplicates when work outlasts the lease |
| 1.3 | A disk dies | Raft per partition, 3 AZs, commit on 2 | Cross-AZ wait; 1–2 s failover |
| 1.4 | One leader is the limit | Partitions spread over brokers | No order across partitions |
| 1.5 | Empty polling | Long polling, pull | Open connections |
| 1.6 | Poison message | maxReceiveCount → DLQ, alarm, redrive | An unwatched DLQ is a graveyard |
Open costs: no order; one consumer team per queue (a message is gone once deleted); producer retries duplicate messages.
R2.1 The Scope Raise
Interviewer: "Your queue worked, and now every team wants it. We're at 2 billion messages a day. Payments needs messages for the same account processed in order. Producer retries must stop creating duplicates. Three teams want the same order events, and analytics wants to replay last week's events after it fixes a bug. Some teams want to send a message now and have it delivered in ten minutes. You must survive losing a whole AZ, and we want 99.99%."
A scope raise is not the end of scoping. Before we fix anything, we ask back, and we say what each answer changes, just as in R1.1.
| We ask | Interviewer answers | What it changes in the design |
|---|---|---|
| Order relative to what: all messages, or per account? | Per account. Two different accounts can be processed in any order. | Order per key, not global. Global order would mean one partition for everything; per-key order keeps parallelism (step 2.1). |
| When a producer retries, how long after the first send can the retry arrive? | The client library retries for about a minute. A producer that restarts may resend a message it wasn't sure about a few minutes later. | A deduplication window of 5 minutes covers both, plus automatic protection for the library's own retries (step 2.2). |
| Do the three teams read at their own pace, and may one team's slowness affect another? | Each at its own pace. Analytics can be hours behind. Nobody may slow anybody else down. | Each team needs its own position in the same stream, which a delete-on-ack queue can't give (step 2.3), and a slow reader must not hurt fast ones (step 2.4). |
| How far back does replay go, and how is "from where" given? | Up to 7 days, by time: "everything since Monday 00:00". | 7 days of retention and a way to find a position by timestamp (steps 2.3 and 2.4). |
| How many teams, and are some much bigger than others? | About 200 teams. The three biggest send 30% of all traffic. | We need per-team quotas, and a plan for keys and teams that are much hotter than the rest (step 2.7). |
| How do consumer teams deploy? | Many times a day, restarting consumers one at a time. | Restarts must not freeze every other consumer in the group each time (step 2.6). |
| How long may a delayed message wait? | Up to 15 minutes, to within about a second. | A per-message delay on task queues: the message is stored at once but stays invisible until its time (R2.3). |
Scope change
| Round 1 | Round 2 | |
|---|---|---|
| Callers | One team | ~200 teams |
| Traffic | 50M/day, ~2K/s peak | 2B/day, ≈ 23K/s average, ≈ 81K/s peak |
| Data kept | Until deleted, at most 4 days | 7 days, whether read or not; replayable |
| Readers per message | One worker | One worker per queue, or several independent teams per stream |
| Order | None | Per key (per account) |
| Duplicates from retries | Allowed | Removed within a 5-minute window |
| Survive | A broker | An AZ |
| Availability | 99.9% (43.8 min/month) | 99.99% (4.4 min/month) |
| Latency, send to a waiting reader | P99 < 100 ms | P99 < 15 ms |
The "Not yet" list from R1.2 is now mandatory: ordering, removing duplicates, delay, many teams reading one message, and replay. Many readers and replay change the design most.
R2.2 What Breaks in the Round 1 Design
| Round 1 choice | What breaks at the new scope |
|---|---|
| Send to any partition | Two payments for one account land in different partitions and are processed in either order. |
| Every send is new | The producer's retry after a lost 200 stores a second copy. At 2 billion messages a day, even 0.01% is 200,000 duplicates a day. |
| Delete on ack | Once the fulfillment team deletes a message, the email and fraud teams can't read it, and nobody can replay last week. |
| One disk path for every reader | Analytics reading 48 hours back pulls old data from disk, pushes recent data out of memory, and slows the real-time readers. |
| Any key, any volume | One huge team, or one very active account, fills its partition while others sit idle. |
| Receipt handle only finds the message | A worker that paused past its lease can delete a message another worker now holds. With per-account order, that releases the next payment early. |
| One fsync per partition log | Round 1 had 6 logs per broker. Now each broker holds hundreds of partition replicas, and an fsync per log per 2 ms would be up to fsyncs a second, more than one broker's EBS path should be asked to do (a gp3 volume now tops out at 80,000 IOPS, and the instance's own EBS IOPS limit is lower). |
R2.3 New Requirements and API Additions
We now have two kinds of topics, and the difference is the heart of this round:
- A queue topic works like Round 1: each message goes to one worker, and a delete removes it for good.
- A stream topic keeps every message for its retention period, whether anyone has read it or not. Each reading team is a consumer group with its own position, and nobody deletes anything.
Send, with order, deduplication and delay (queue or stream topic):
httpPOST /v1/topics/payments/messages HTTP/1.1 Host: mq.internal Content-Type: application/json { "body": "{\"accountId\": \"acct_42\", \"paymentId\": \"pay_9001\", \"amountCents\": 1299}", "groupKey": "acct_42", "deduplicationId": "pay_9001", "delaySeconds": 0 }
httpHTTP/1.1 200 OK Content-Type: application/json { "messageId": "m-17-5520391", "partition": 17, "offset": 5520391, "duplicate": false }
groupKeydecides the partition, so every message with the same key lands in the same partition, in order (step 2.1). It plays the role of SQS FIFO'sMessageGroupId.deduplicationId: a second send with the same ID within 5 minutes stores nothing and returns the first message's ID with"duplicate": true(step 2.2).delaySeconds(0 to 900) is only allowed on queue topics without agroupKey. A delayed message is stored at once but stays invisible until its time. On an ordered group it would block every message behind it, so ordered queues don't take per-message delays (SQS FIFO has the same rule: delay only per queue).
Batch send and batch receive. POST /v1/topics/{t}/messages:batch takes up to 100 messages, and a receive can return up to 100. A batch shares one request, one TLS record and one group-commit wait.
Stream topics: fetch by offset, commit the offset.
httpPOST /v1/topics/order-events/groups/email-service/partitions/4:fetch HTTP/1.1 Host: mq.internal Content-Type: application/json { "fromOffset": 88120775, "maxBytes": 1048576, "waitMs": 500 }
httpHTTP/1.1 200 OK Content-Type: application/json { "messages": [ { "offset": 88120775, "body": "..." }, { "offset": 88120776, "body": "..." } ], "highWatermark": 88121940 }
httpPOST /v1/topics/order-events/groups/email-service/offsets HTTP/1.1 Host: mq.internal Content-Type: application/json { "partition": 4, "offset": 88120777 }
The committed offset is the position of the next message the group will read. The high watermark is the offset of the last committed message plus one: consumers never see anything that isn't on a majority yet.
Seek, by offset or by time, for replay:
httpPOST /v1/topics/order-events/groups/analytics/seek HTTP/1.1 Host: mq.internal Content-Type: application/json { "toTimestamp": "2026-09-21T00:00:00Z" }
The broker finds, in each partition, the first message at or after that time (step 2.4 explains how) and sets the group's offsets there. { "partition": 4, "toOffset": 88000000 } seeks one partition to an exact offset instead. A group must have no active consumers while it seeks, so two instances can't disagree about where they are.
Quotas. Each team has a produce rate and a fetch rate, in bytes per second. Over quota, the answer is 429 Too Many Requests with Retry-After (step 2.7).
Two consumption contracts, side by side
| Queue topic (delete on ack) | Stream topic (commit offset) | |
|---|---|---|
| Who gets a message | One worker, whoever asks first | Every consumer group, each at its own pace |
| How work is split | Any worker can get any message | Each partition belongs to one consumer of the group at a time |
| What "done" means | DELETE with the receipt handle; that message is gone | Commit the offset; everything before it is done for this group |
| A failed message | Its lease expires and it comes back alone; the DLQ takes it after 5 tries | The consumer retries it or parks it; the partition doesn't move on until it does |
| Order | Per groupKey, one message of a group in flight at a time | Per partition, always |
| When data goes away | When deleted, or after its retention | Only when retention expires |
| Replay | No | Yes: seek to an offset or a time |
| Round 1's queue | This is it | New |
R2.4 Design Evolution: Order, Duplicates, Fan-Out and Replay
Step 2.1: Payments for One Account Must Be Processed in Order
The problem: account acct_42 sends "authorize $12.99" and then "capture $12.99". The two messages land in different partitions, and a worker processes the capture first. It fails, because nothing has been authorized yet.
What would you do? How do we keep order for each account without losing parallelism?
Primitive: Database Sharding & Partition Keys
Step 2.2: A Producer Retry Created a Duplicate
The problem: the payments service sends "capture pay_9001". The broker stores it, but the 200 is lost on the way back. The client library times out and sends again. Now the capture is in the queue twice, and payments has to catch it downstream, every time.
What would you do?
sql-- In the consumer's own database, committed together with the business change CREATE TABLE processed_messages ( dedup_id VARCHAR(128) PRIMARY KEY, -- the message's deduplicationId processed_at TIMESTAMP NOT NULL );
If a consumer keeps its idempotency keys in DynamoDB instead, it claims each key with a conditional put (attribute_not_exists(dedup_id)), and may use DynamoDB TTL to clean up old keys. TTL is fine for cleanup but never for correctness: it deletes expired items only eventually, typically within a few days, so a key can live well past its TTL. Keep keys at least as long as the longest possible redelivery (here, 7 days of retention) and treat anything older as noise.
Primitive: Change Data Capture & Outbox Pattern · Drill: CDC outbox dual-write drift
Step 2.3: Three Teams Want the Same Events, and Analytics Wants Last Week Again
The problem: fulfillment, email and fraud all need every order event. Fulfillment's delete removes the message before email sees it. And analytics, after fixing a bug in its pipeline, wants to reprocess every order event since Monday. What would you do?
| Queue (delete on ack) | Log (commit offset) | |
|---|---|---|
| Unit of work | One message | A position in a partition |
| Reading changes the data | Yes: leases, then deletes | No: only the group's offset moves |
| Readers | Competing workers share one queue | Any number of independent groups |
| Replay | Impossible | Seek to an offset or a time |
| A slow message | Only that message waits | The partition's later messages wait for this consumer |
| Storage | Only unprocessed messages | Everything for the retention period |
| Examples | SQS, RabbitMQ queues | Kafka, Kinesis Data Streams |
Primitive: Message Queues vs Event Streams
Step 2.4: A Replaying Reader Slowed Everyone Else Down
The problem: analytics starts its replay from 48 hours ago. Within minutes, the payments team's P99 latency jumps from 10 ms to over a second. What would you do?
Synthesizing vector architecture diagram...
Three kinds of reader, three paths. Only the live readers use the page cache, so nothing the other two do can push their data out of it.
textSparse indexes for one segment (base offset 5,520,000) .index relative offset → byte position one entry every ~4 KB of log (about 2 messages of 2 KB) 0 → 0 2 → 4,096 4 → 8,192 ... .timeindex timestamp (ms) → relative offset one entry every ~4 KB of log 1790424000000 → 0 1790424000001 → 2 1790424000002 → 4 find offset 5,520,005: relative offset 5; newest index entry ≤ 5 is 4 → start reading at byte 8,192, skip record 4, return record 5
Primitive: Message Queues vs Event Streams
Step 2.5: A Stale Worker Deleted a Message Another Worker Now Holds
The problem: worker A receives acct_42's "authorize" message on the payments queue, with a 30-second lease. A long garbage-collection pause freezes A for 40 seconds. At second 30 the lease expires and worker B receives the same message. At second 40, A wakes up and calls DELETE with its receipt handle. The broker deletes the message and releases the group, so the next message, "capture", goes to worker C while B is still authorizing.
What would you do?
Primitive: Distributed Locks & Leases · Drill: Distributed lock fencing token
Step 2.6: Consumer Groups Freeze Every Time One Consumer Restarts
The problem: the email team deploys twenty times a day, restarting its 24 consumers one at a time. Each restart makes the whole group stop, give back all its partitions and take them again. Every deploy means minutes of email delays. What would you do?
Step 2.7: One Tenant's Hot Key Is Starving Others
The problem: a marketplace account, acct_1, sends 30% of all payment messages. They all hash to partition 12, whose leader is maxed out. Every other account unlucky enough to share partition 12 waits behind it.
What would you do?
Primitive: Distributed Rate Limiting · Drill: Sharding tenant hotspot
Round 2 Step Summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 2.1 | Order per account | hash(groupKey) mod P; one message per group in flight | Head-of-line blocking; a hot group caps itself; fixed P |
| 2.2 | Retries duplicate | Idempotent producer (ID + sequence) and a 5-min deduplicationId window; idempotent consumers | ~2.4 GB of IDs at most; a window, not forever |
| 2.3 | Fan-out and replay | Stream topics: 7-day retention, consumer groups, committed offsets | 100.8 TB; offsets; rebalancing |
| 2.4 | Replays hurt live readers | Tiered storage: 24 h on brokers, 7 days in S3; separate cold read path; time index | Slower replays per partition |
| 2.5 | Stale delete | Fenced, signed receipt handles; replicated lease records | Clients handle 409; a small write per receive |
| 2.6 | Rebalance freezes | Cooperative rebalancing; static membership; tuned session timeout | Slower detection of dead consumers |
| 2.7 | Hot tenant or key | Quotas (429), sub-keys, isolation | Caller complexity; weaker order when split |
R2.5 Architecture v2
Synthesizing vector architecture diagram...
Producers ask once where each partition's leader is, then talk to leaders directly; the NLB only helps them find the cluster. Inside the fleet, every partition is a Raft group with one replica per AZ. Stream consumers read from the replica in their own AZ, which keeps their traffic from crossing AZs (R2.6). Sealed segments flow down to S3, and reads older than 24 hours flow back up from it without touching the brokers' page cache.
The pieces that changed:
- 12 brokers, 4 per AZ, each an
m7g.2xlarge(8 vCPUs, 32 GiB) with a 2.5 TB gp3 volume. About 2,000 partitions across all topics, each with 3 replicas: replicas per broker. - Controllers replace Round 1's metadata store: the same kind of 3-node Raft group, now also holding quotas and consumer-group assignments.
- Clients connect to partition leaders directly. At this scale, sends for a key must reach that key's leader, so a load balancer in the middle would only add a hop.
- Consumers fetch from followers in their own AZ. A follower only serves messages up to the high watermark it has heard from the leader, so it never shows uncommitted data; it adds a millisecond or two. (Kafka supports the same thing with
client.rack.)
The write path at this scale: one journal per broker. R2.2 found that an fsync per partition log no longer fits: 500 logs per broker, each fsyncing every few milliseconds, would need tens of thousands of disk operations a second. So each broker now writes every incoming record, for all its partitions, to one shared journal file first. The journal is fsynced once per 2 ms window (or 256 KB), which is at most 500 fsyncs a second per broker, whatever the partition count. Only after the journal fsync does a replica count as "has it". Records are then copied into each partition's own segments in memory, and those segment files are written to disk in the background without waiting. If a broker crashes, it replays the journal to rebuild the segments' tails; once segments are safely on disk, the journal is trimmed. Databases call this group commit to a write-ahead log, and Apache BookKeeper (the storage under Apache Pulsar) splits its journal from its data files the same way.
Heartbeats for hundreds of Raft groups are also batched: between any two brokers, one message carries the heartbeats of every partition they share.
Trace 1: an ordered, deduplicated send.
Synthesizing vector architecture diagram...
The retry never reaches the log. If the payments service had restarted instead and resent with a new producer ID, the 5-minute pay_9001 entry would catch it the same way.
Trace 2: a replay from a timestamp.
Synthesizing vector architecture diagram...
The replay only ever touches S3 and the separate cold read path until it reaches recent data, so the payments team's latency doesn't move.
R2.6 Numbers and Cost
Traffic and storage, re-derived from the current track's figures:
| Quantity | Math | Value |
|---|---|---|
| Messages per day | given | 2,000,000,000 |
| Average rate | 2 × 10⁹ ÷ 86,400 s | ≈ 23,148 msg/s |
| Peak rate | 23,148 × 3.5 | ≈ 81,000 msg/s |
| Bytes in at peak | 81,000 × 2 KB | 162 MB/s ≈ 1.296 Gbps |
| Bytes out to consumers at peak | 2 consumer groups per topic on average × 162 MB/s | 324 MB/s ≈ 2.59 Gbps |
| Bytes per day | 2 × 10⁹ × 2 KB | 4 TB/day |
| 7 days, one copy | 4 TB × 7 | 28 TB |
| 7 days, 3 replicas, 20% overhead | 28 TB × 3 × 1.2 | 100.8 TB |
Two corrections to the track's figures. First, the 20% isn't really "index": a sparse index has one small entry (8 bytes in the offset index, 12 in the time index) per ~4 KB of log, well under 1%. The 20% is record headers, lease and ack records, and free space, and it's still a reasonable planning figure. Second, the track's single "P99 4.8 ms" for a send isn't derived from anything; the budget below is.
Per-broker load. Every message is written to 3 replicas, so the fleet takes in MB/s at peak (from producers, plus replication) and sends out MB/s (to consumers, plus replication out of leaders). Spread over the brokers:
| 12 brokers (normal) | 8 brokers (an AZ lost; conservative: assumes full replication traffic, which also covers the returning AZ's catch-up) | |
|---|---|---|
| Network in per broker | 486 ÷ 12 ≈ 40.5 MB/s | 486 ÷ 8 ≈ 60.8 MB/s |
| Network out per broker | 648 ÷ 12 = 54 MB/s | 648 ÷ 8 = 81 MB/s |
| Total per broker | ≈ 94.5 MB/s ≈ 0.76 Gbps | ≈ 141.8 MB/s ≈ 1.13 Gbps |
| Share of the instance's 3.75 Gbps baseline network | ≈ 20% | ≈ 30% |
| Disk writes per broker (journal + segments, so 2×) | ≈ 81 MB/s | ≈ 122 MB/s |
| Leader messages per broker at peak | 81,000 ÷ 12 = 6,750/s | 81,000 ÷ 8 ≈ 10,100/s |
After an AZ loss, each partition still has 2 of its 3 replicas, which is a majority, so commits continue. The headroom above 30% is for catch-up readers, for the returning AZ copying what it missed (which we throttle), and for TLS. The gp3 volumes are provisioned at 250 MB/s (baseline is 125 MB/s), and the instance's own EBS baseline is 312.5 MB/s. These per-broker figures are planning numbers; we confirm them with a load test before launch.
Partitions. About 200 teams' topics, averaging 10 partitions each, gives about 2,000 partitions, or 500 replicas per broker. As a sanity check, AWS recommends at most 2,000 partition replicas per broker for Amazon MSK's kafka.m7g.2xlarge; we're a different system, but the order of magnitude is right. Throughput isn't what sets the count: at a planning ceiling of 5 MB/s per partition, 162 MB/s needs only 33. The count is set by how many consumers each team wants to run in parallel, since a group can't use more consumers than partitions.
Page cache. Of each broker's 32 GiB, about 24 GiB is left for the page cache. At 40.5 MB/s of writes, that holds seconds, about 10 minutes, of everything the broker stores. Live readers are seconds behind, so they're served from memory.
What the S3 tier offloads
| All 7 days on brokers | 24 hours on brokers, 7 days in S3 | |
|---|---|---|
| Broker disk used | 100.8 TB (8.4 TB per broker) | 4 TB × 3 × 1.2 = 14.4 TB (1.2 TB per broker) |
| Broker disk provisioned (~48% full) | ≈ 210 TB | 12 × 2.5 TB = 30 TB |
| In S3 | none | 28 TB (one copy: S3 keeps its own redundancy across AZs) |
| Monthly storage cost | 210,000 GB × $0.08 ≈ $16,800 | $2,460 (EBS) + $666 (S3) ≈ $3,126 |
Tiered storage cuts the brokers' disk by 86% and saves about $13,700 a month. The 2.5 TB volumes are 48% full, which leaves room for a whole day of S3 upload failures before a disk fills.
Cross-AZ traffic ($0.01/GB in each direction, so $0.02 per GB that crosses; 4 TB/day × 30.4 = 121.6 TB a month):
| Flow | Math | Monthly |
|---|---|---|
| Replication: every message to 2 followers in other AZs | 2 × 121.6 TB × $0.02/GB | ≈ $4,864 |
| Producers to leaders: about 2/3 of producers are in another AZ than their leader | 2/3 × 121.6 TB × $0.02/GB | ≈ $1,621 |
| Consumers, reading from the leader | 2/3 × 243.2 TB × $0.02/GB | ≈ $3,243 |
| Consumers, reading from a follower in their own AZ (chosen) | almost nothing crosses | ≈ $0 |
Fetching from followers saves about $3,200 a month. Replication traffic can't be avoided: it is the durability.
Latency budget: P99 from send to a waiting reader under 15 ms.
| Piece | Estimate |
|---|---|
| Producer batching wait (payments topics set it to at most 1 ms) | ≤ 1 ms |
| Producer to leader | ≤ 1 ms |
| Leader's journal window and fsync, in parallel with the follower path below | ≤ 2 + 3 ms |
| To a follower in another AZ, its journal window and fsync, and back | ≤ 1 + 2 + 3 + 1 = 7 ms |
| Committed message to a waiting reader (a follower learns the commit on its next fetch) | ≤ 1–2 ms |
| Total | ≈ 10–11 ms |
That leaves about 4 ms, and the least predictable piece is the fsync on network block storage. If load tests show its P99 breaking the budget, the lever is the one Kafka uses by default: count a replica as "has it" once the message is in its memory, and flush to disk in the background. Messages are then lost only if replicas in two AZs lose power before the flush. That's a real trade, and we'd make it per topic, in writing, not silently for everyone.
Availability budget. 99.99% is 4.4 minutes a month. A broker crash makes its ~167 leader partitions refuse sends for 1 to 2 seconds while followers elect new leaders; producers retry within their timeout, so callers rarely see it. The budget goes to what retries can't hide: a bad deploy, a misconfigured quota, a controller problem.
Monthly cost (us-east-1, on-demand)
| Item | Math | Monthly |
|---|---|---|
12 × m7g.2xlarge brokers | 12 × $0.3264/h × 730 h | ≈ $2,859 |
3 × m7g.large controllers | 3 × $0.0816/h × 730 h | ≈ $179 |
| 12 × 2.5 TB gp3, +125 MB/s each | 30,000 GB × $0.08 + 12 × 125 × $0.04 | ≈ $2,460 |
| S3: 28 TB plus uploads | 28,000 GB × $0.023 + ~4.4M PUTs × $0.005/1,000 = $644 + $22 | ≈ $666 |
| Cross-AZ replication | from the table above | ≈ $4,864 |
| Cross-AZ producers | from the table above | ≈ $1,621 |
| NLB (bootstrap only) | ≈ $20 | |
| CloudWatch metrics, alarms, logs | ≈ $150 | |
| Total | ≈ $12,800/month |
The single biggest line is replication traffic between AZs: more than the brokers themselves.
Build vs buy at this size.
| Option | What it costs | What it covers |
|---|---|---|
| Our fleet | ≈ $12.8K/month, plus a team on call | Queue topics and stream topics in one system |
| Amazon MSK (managed Kafka), same shape | 12 × $0.816/h × 730 h ≈ $7,148 for brokers; 30 TB × $0.10 ≈ $3,000 for broker storage; 28 TB × $0.06 ≈ $1,680 for MSK's tiered storage; replication between brokers isn't charged; producers' cross-AZ ≈ $1,621 → ≈ $13.4K/month | Stream topics, with fetch-from-follower. Not visibility-timeout task queues. |
| Amazon SQS (standard or FIFO) | Per request; FIFO high-throughput mode takes up to 70,000 requests a second per API action in us-east-1, or 700,000 messages with batches of 10 | Task queues with visibility timeouts, DLQs and redrive. No replay, and fan-out needs SNS in front with one queue per team. |
The honest answer for most companies: MSK for the streams, SQS for the task queues. MSK costs about what our fleet does, without the on-call rotation, because its free replication traffic offsets its higher broker price. We would build only if we need something neither offers, and in Round 3 we'll see whether scale changes that.
R2.7 Trade-Offs
| Choice | We chose | What we give up |
|---|---|---|
| Queue vs log | Both: queue topics for tasks, stream topics for events | Two contracts to explain and support. A log alone makes per-message retries and DLQs awkward; a queue alone can't fan out or replay. |
| FIFO per group vs parallelism | FIFO per groupKey, one message of a group in flight | A slow message blocks its group; one group runs at one worker's speed. Global FIFO would be one partition for everything. |
| Dedup window length | 5 minutes | Longer catches later resends but costs memory and failover time; shorter misses slow restarts. Consumers stay idempotent either way. |
| Quorum acks vs leader-only acks | Commit on 2 of 3 | One cross-AZ round trip per send. Leader-only acks (Kafka's acks=1) are faster but lose anything the leader stored alone when it dies. |
| fsync before "has it" vs replicate-then-flush | fsync, through one journal per broker | A few ms per send and double disk writes. Replicate-then-flush is faster but can lose recent acknowledged messages if two AZs lose power together. |
| Push vs pull | Pull, with long polling and fetch waits | Push could shave a millisecond but floods slow consumers without extra flow control. |
| Build vs MSK / SQS | Build, for the interview; MSK and SQS in most real companies | Building means a team on call; buying means their limits and prices. |
Acknowledgment levels. When does the leader answer the producer? Kafka names the options with its acks setting, and every log-based system makes the same choice:
| Level | Leader answers when | If the leader dies right after answering | Latency |
|---|---|---|---|
acks=0 | Immediately, before storing anything | The message may never have been stored | Lowest |
acks=1 (leader only) | The leader has stored it | Lost, if no follower had copied it yet | Low |
Majority (ours) or Kafka's acks=all | 2 of 3 replicas have it (Kafka: every in-sync replica, with at least min.insync.replicas) | Safe: another replica has it and will be elected | One cross-AZ round trip more |
The alternatives, side by side (figures from AWS's documentation at the time of writing):
| Amazon SQS standard | Amazon SQS FIFO | Apache Kafka / Amazon MSK | RabbitMQ | |
|---|---|---|---|---|
| Model | Task queue: lease, delete on ack | Ordered task queue | Log: consumer groups commit offsets | Broker routes messages to queues; also offers log-style streams |
| Order | Best effort | Per MessageGroupId | Per partition | Per queue, for a single consumer |
| Replay | No | No | Yes, within retention | Queues no; streams yes |
| Throughput | Nearly unlimited API calls per second per action | 300 calls/s per partition per API action (3,000 messages with batches of 10); high-throughput mode up to 70,000 calls/s per API action in us-east-1, us-west-2 and eu-west-1 (700,000 messages with batches), less in other regions | Grows with partitions and brokers; a cluster's limit is its brokers' network and disks | Depends heavily on queue type, message size, persistence and publisher confirms; benchmark it rather than trust a quoted range |
| Retention | 60 s to 14 days (default 4) | 60 s to 14 days | Configurable; long with tiered storage | Until consumed (queues); by size or age (streams) |
| Largest message | 1 MiB | 1 MiB | About 1 MB by default, configurable | Configurable |
| Dedup | No | 5-minute window by deduplication ID | Idempotent producer; transactions | Publisher confirms; no built-in dedup window |
R2.8 Failure Modes
| Failure | Trigger | What you'd see | How the design responds |
|---|---|---|---|
| Losing an AZ | An AZ-wide outage | A third of the brokers gone; a burst of leader elections; client reconnects | Every partition keeps 2 of 3 replicas, a majority, so it elects a leader in the other two AZs and keeps committing. The 8 remaining brokers run at about 30% of network baseline (R2.6). When the AZ returns, its replicas copy what they missed, throttled so the copy doesn't crowd out live traffic. Until then, one more failure in a partition stops it. |
| A poison message in a FIFO group | A payment that always fails | One account's payments stop; its group's oldest-message age climbs | After 5 receives the broker moves it to the DLQ and releases the group. For payments that may be wrong: the next message would run without the one before it. So each ordered topic chooses: move to DLQ and continue (most topics), or block the group and page (payments). |
| Rebalance storm | Consumers flapping: crash loops, pauses longer than the session timeout | A group's lag rises in steps; repeated assignment changes | Cooperative rebalancing limits each change to the moved partitions; static membership stops restarts from rebalancing. We alarm on rebalances per hour per group and fix the flapping consumer. |
| Page-cache thrashing | A big replay or many lagging readers | Live readers' P99 jumps; disk reads rise | Reads older than 24 h go to S3 through the separate path; catch-up reads from disk don't stay in the cache (step 2.4). |
| Connection exhaustion | Thousands of short-lived clients, such as functions, each opening a new TLS connection | Broker CPU spent on handshakes; connection counts near limits; latency rises | The client library keeps a few long-lived connections per broker; each team has a connection quota; brokers reject new connections over a limit rather than fall over. |
| Dedup state on failover | A partition leader dies | A second or two of 503s; then normal | The new leader rebuilds the dedup map from the last 5 minutes of its log, and the producer sequence state from the stored batches. Nothing that was committed can be duplicated by a retry. A send the old leader never committed is simply gone, and the producer's retry stores it once, correctly. Duplicates only get through if a resend comes after the 5-minute window. |
R2.9 Production Gotchas
| Gotcha | Symptom | Cause | Fix |
|---|---|---|---|
| Visibility timeout set to the average processing time | Many messages processed twice, worse at peak | Half of all messages take longer than the average, so their leases expire mid-work | Set the timeout well above the slowest normal processing time and have workers extend their lease while they work |
| Short polling | Most receives come back empty; on SQS, a bill for millions of empty requests | Receives with a wait of 0 in a tight loop | Long polling with the maximum wait (20 s) |
| An unwatched DLQ | Customers report lost orders weeks later | Messages sat in the DLQ until they expired | Alarm on DLQ depth above zero, give every DLQ an owner, and redrive after fixing |
| Using the queue as a database | Teams scan topics to answer "what's the status of order 42?"; long retention for lookups | A queue has no index by business key and forgets after retention | Consumers write to a real database (DynamoDB, Aurora, or S3 with a table format) and query that |
| Offsets committed before processing | After a consumer crash, messages were skipped, not repeated | Auto-commit on a timer while work was handed to other threads, so offsets moved past messages still in progress | Commit only offsets whose messages are fully processed |
R2.10 Pillar Check
| Pillar | What Round 2 adds |
|---|---|
| Reliability | Every partition keeps a majority through an AZ loss; fenced receipt handles and replicated lease records; the idempotent producer and dedup window; per-topic choice between DLQ-and-continue and block-and-page for ordered groups REL 10 · REL 11 · REL 4 · REL 5 |
| Performance Efficiency | Batches, one journal per broker, a page cache kept for live readers, cold reads from S3 on their own path, fetch from a replica in the same AZ; a latency budget with headroom PERF 3 · PERF 4 · PERF 5 |
| Security | Permissions per topic and per consumer group: which team may produce, which may read, which may seek back; mutual TLS with a certificate per service, so every connection has an identity; quotas so one team can't starve another; S3 segments and EBS volumes encrypted with KMS SEC 2 · SEC 3 · SEC 8 · SEC 9 |
| Cost Optimization | About $12.8K/month, derived; tiered storage saves ~$13.7K a month; fetch-from-follower saves ~$3.2K; the honest comparison with MSK and SQS COST 5 · COST 8 · COST 11 |
| Operational Excellence | Alarms with a first action each (below); consumers deploy without stopping their group OPS 8 · OPS 6 |
| Sustainability | Light this round: one stored copy serves every team instead of a copy per team; 7-day retention deletes data nobody needs; Graviton brokers SUS 4 · SUS 5 |
Alarms and first actions
| Signal | Alarm | First action |
|---|---|---|
| Consumer lag per group (messages, and seconds behind) | Above the group's own target for 10 min | Is the group consuming at all? Check rebalances, errors and the downstream it writes to |
| Oldest message age on a queue topic | > 5 min | Check the workers and their downstream dependency |
| DLQ depth | > 0 | Page the owning team; look at the first message's error |
| Under-replicated partitions | any for 5 min | Find the lagging broker: disk, network or a stuck replica |
| Rebalances per group | > 10 an hour | Find the flapping consumer (crash loop, long pauses) |
| Quota throttling per team | sustained | Ask whether the team's traffic grew legitimately; raise the quota or find the bug |
| Journal fsync P99 | > 5 ms | Check the EBS volume's latency and throughput limits on that broker |
R2.11 Round 2 Rubric and Follow-Ups
What a senior (L6) answer adds over L5
- Separates the two consumption models, queue and log, explains why each exists, and supports both.
- Gives order per key, not globally, and names head-of-line blocking and hot groups as its costs.
- Stops duplicates at the door with an idempotent producer and a dedup window, and still says processing is at-least-once.
- Protects live readers from replays with tiered storage and a separate cold read path.
- Fences stale workers with lease epochs, and keeps lease state across leader changes.
- Knows why consumer groups freeze and fixes it with cooperative rebalancing and static membership.
- Sizes the fleet for an AZ loss and finds that cross-AZ traffic is the biggest line on the bill.
Follow-up questions
-
"How do you build an exactly-once pipeline when the queue is at-least-once?" Answer: decide where "once" must hold. Inside the log, a read-process-write job can be exactly-once with Kafka-style transactions: the idempotent producer, plus a transaction that writes the output messages and the consumer's new offsets atomically, plus downstream readers that only read committed transactions (
read_committed). If the job crashes, the transaction is aborted and redone, and readers never see the half-done attempt. That guarantee stops at the log's edge. The moment the effect is outside it (charging a card, sending an email, updating a database), we need idempotency there: the idempotency-key table in the same transaction as the effect, or the outbox. End to end, "exactly once" is always at-least-once delivery plus idempotent effects. -
"Why is Kafka so fast on ordinary disks?" Answer: four reasons. Sequential I/O: it only appends to the end of files and reads forward, which is the fastest thing disks do. The page cache: it lets the operating system keep recent data in memory instead of keeping its own cache, so live readers rarely touch the disk. Batching: producers send, brokers store and consumers fetch messages in batches, often compressed, which spreads each request's fixed cost over many messages. Zero-copy: for plaintext connections, the broker uses the
sendfilesystem call, so data goes from the page cache to the network card without being copied into the broker's own memory. The caveat: with TLS, the broker must encrypt the data itself, so it has to copy it into its own memory, and plain zero-copy is lost. Linux kernel TLS can move encryption into the kernel sosendfileworks again, but that depends on the runtime and the platform, so don't assume it. -
"The payments topic needs more partitions. Can we just add them?" Answer: not without breaking order for a while.
hash(groupKey) mod Pchanges for most keys whenPchanges, soacct_42's next payment could land in a new partition and be processed before older ones still waiting in the old partition. The safe way is a cutover: create the new layout, pause producers for the moved keys (or the whole topic, briefly), let consumers drain the old partitions for those keys, then resume on the new layout. Better still, create ordered topics with enough partitions for years of growth, since idle partitions cost little.
Interview gotchas from this round
| Gotcha | Why it's wrong |
|---|---|
| "Use one partition to get order" | That's global order and one leader's throughput. The ask is per account. |
| "Dedup means exactly-once" | Dedup stops duplicate sends. A crash between the effect and the delete still redelivers. |
| "Copy the events into a queue per team" | Triples storage and writes, can't serve a new team's history, and can't replay. |
| "More RAM fixes slow replays" | A replay reads terabytes; it evicts the live readers' data whatever the RAM. Separate the read paths. |
| "Kafka's exactly-once is end to end" | It covers reads and writes inside Kafka. External effects need idempotency. |
| "Zero-copy always works" | Not with TLS terminated in the broker. |
Round 3 · Architect · "Global, Regulated, and at Petabyte Retention"
~45 min · Principal (L7) · 4 regions · ~810K msg/s peak · 99.999% per region · RPO of seconds for critical topics
R3.0 Where We Left Off
This is what the candidate says aloud in the first 60 seconds of Round 3. If you're starting here, it's everything you need from Rounds 1 and 2.
Round 2 in 60 seconds. "We run the company's messaging platform in one region across three AZs: 2 billion messages a day, about 81,000 a second at peak, 2 KB on average, 99.99%. There are two kinds of topics. Queue topics work like SQS: a message is leased to one worker with a visibility timeout, deleted on success, moved to a DLQ after 5 tries. Stream topics work like Kafka: messages stay 7 days, and each consumer group commits its own offsets, so many teams read the same data and can replay it by time. Order is per group key: the key hashes to a partition, and a queue hands out one message of a group at a time. Producers are idempotent, with IDs and sequence numbers, and there's a 5-minute dedup window. Receipt handles carry a lease epoch, so a stale worker can't delete a message someone else holds. Every partition is a Raft group with a replica in each AZ, committing on 2 of 3, through one fsynced journal per broker. Brokers keep 24 hours locally and 7 days in S3, and replays read from S3 on their own path. Twelve
m7g.2xlargebrokers, about $12,800 a month, with cross-AZ replication as the biggest line; managed MSK costs about the same. Four costs are still open: it's one region; offsets and order don't exist outside it; long retention would be expensive; and we can't delete one customer's data from an append-only log."
Architecture v2, compact
text~200 teams (client library: producer ID + sequence, dedup IDs, batches, quotas → 429) │ bootstrap via NLB, then straight to partition leaders ▼ 12 × m7g.2xlarge, 4 per AZ · ~2,000 partitions · each a Raft group of 3 (one per AZ), commit on 2 │ one journal per broker, fsync per 2 ms window; per-partition segments written behind it │ queue topics: leases with epochs (replicated lease records), DLQ after 5 │ stream topics: 7-day retention, consumer groups, __offsets (compacted) ├── sealed segments hourly ──► S3 (7 days); reads older than 24 h come back from S3 └── controllers: 3-node Raft group (topics, leaders, quotas, group assignments) consumers fetch from the replica in their own AZ
Steps so far
| Step | Problem | Component |
|---|---|---|
| 1.1 | Restart loses messages | Segment log, fsync, group commit, ack records |
| 1.2 | Worker crashes | Visibility-timeout lease |
| 1.3 | Disk dies | Raft per partition, 3 AZs, commit on 2 |
| 1.4 | One leader is the limit | Partitions |
| 1.5 | Empty polling | Long polling |
| 1.6 | Poison message | DLQ with alarm and redrive |
| 2.1 | Order per account | hash(groupKey) mod P, one message per group in flight |
| 2.2 | Retry duplicates | Idempotent producer; 5-minute dedup window; idempotent consumers |
| 2.3 | Fan-out and replay | Stream topics, consumer groups, offsets |
| 2.4 | Replays hurt live readers | Tiered storage to S3; separate cold read path |
| 2.5 | Stale delete | Fenced receipt handles; replicated lease records |
| 2.6 | Rebalance freezes | Cooperative rebalancing; static membership |
| 2.7 | Hot tenant or key | Quotas, sub-keys, isolation |
Open costs: one region is one blast radius; offsets, order and dedup stop at the region's edge; long retention is expensive; one customer's data can't be removed from an immutable log.
R3.1 The Scope Raise
Interviewer: "The platform now runs in four regions, and we must survive losing any one of them. For critical topics, we can lose at most a few seconds of messages. EU tenants' messages must stay in the EU. Finance must keep some topics for 90 days, and legal sometimes puts data on hold. A customer can demand that we delete their data. We're at 20 billion messages a day. And the platform's bill is now a board-level question."
Again, we ask back before we design:
| We ask | Interviewer answers | What it changes in the design |
|---|---|---|
| Which topics must survive a region loss, and how many seconds may we lose? | Critical topics (payments, orders), about 25% of traffic: at most about 5 seconds. The rest may wait for their region to come back. | Mirroring is a per-topic policy, and it's asynchronous: a few seconds of lag is allowed (step 3.1). |
| Which four regions, and which may EU data use? | us-east-1, us-west-2, eu-west-1 and eu-central-1. EU tenants' data may be stored and processed only in the two EU regions. | Regions come in pairs: us-east-1 with us-west-2, eu-west-1 with eu-central-1. EU data mirrors only inside the EU pair (step 3.3). |
| When a region fails, do its applications move too? | Critical services fail over to the paired region. Everything else waits for its region. | The paired region takes the critical producers and consumers, and must be sized for them (R3.6). Order across that move is the hard part (step 3.2). |
| Which data needs 90 days, and must old data be readable after a region loss? | Finance topics, about 20% of traffic. Data older than 7 days may be unavailable while its region is down, as long as it isn't lost. | Only 7 days need to exist in the paired region. The 90-day archive stays in its home region, in S3 (step 3.4). |
| What does a deletion request cover, and how fast must it happen? | Two kinds: a tenant leaving the platform, and one end customer's data inside shared topics. Legal sets the deadlines: think weeks, not seconds. | We need to delete a tenant's data and a single customer's data from logs we never rewrite, including S3 copies and mirrors (step 3.3). |
| How does legal hold work? | Rare, a few tenants or topics at a time, kept until legal releases them. A hold beats retention and deletion. | Retention and deletion must check holds before they remove anything (R3.3). |
| What does the board want to see? | The monthly bill, and what a managed service would cost instead. | Step 3.6 and R3.6: the numbers, and a recommendation. |
Scope change
| Round 2 | Round 3 | |
|---|---|---|
| Footprint | 1 region, 3 AZs | 4 regions in 2 pairs, 3 AZs each |
| Traffic | 2B/day, ~81K/s peak | 20B/day, ≈ 231K/s average, ≈ 810K/s peak |
| Data kept | 7 days | 7 days for most topics; 90 days for finance; legal holds |
| Survive | An AZ | A region, losing at most ~5 s of critical topics |
| Where data may live | Anywhere in the region | EU tenants: EU regions only |
| Deletion | By retention only | + a tenant's data, and one customer's data, on request |
| Availability | 99.99% | 99.999% per region (about 26 s/month) |
| Cost | A line item | A board-level number, compared with managed options |
R3.2 What Breaks in the Round 2 Design
| Round 2 choice | What breaks at the new scope |
|---|---|
| One region | If the region fails, every topic stops, and data that exists only there is out of reach. |
| One cluster per region | One bad configuration push or broker release reaches every topic and every tenant at once. |
| Offsets as positions in one log | A copy of a topic in another region has different offsets. A consumer that moves regions doesn't know where to resume. |
| Order per key, dedup per partition leader | Both stop at the region's edge. A key written in two regions has no single order, and a retry in the other region isn't recognized. |
| Tiered storage sized for 7 days | 90 days of everything at 3 replicas on brokers would be 40 TB/day × 90 × 3 × 1.2 ≈ 13 PB. Even in S3 Standard, long retention for every topic is a large bill. |
| Deleting by retention | Retention deletes whole segments after a fixed time. It can't delete one customer's messages from the middle of a segment, or a tenant's data before its time. |
| Mirroring would copy anywhere | Nothing stops an EU tenant's topic from being mirrored to a US region. |
R3.3 New Requirements and API Additions
A topic policy carries everything this round adds: home region, residency, mirroring, retention tiers and encryption.
json{ "topic": "payments-eu", "tenant": "t_bank_eu_17", "homeRegion": "eu-west-1", "residency": "EU", "replication": { "mirrorTo": ["eu-central-1"], "targetLagSeconds": 5 }, "retention": { "brokers": "24h", "s3Standard": "7d", "s3GlacierInstantRetrieval": "90d" }, "encryption": { "tenantKey": "arn:aws:kms:eu-west-1:111122223333:key/mrk-1234abcd12ab34cd56ef1234567890ab", "perCustomerKeys": true }, "ordered": true }
homeRegionis the one region that accepts writes for this topic's keys (step 3.2).residencylimits where the topic, its mirrors, its S3 objects and its keys may live. A policy that mirrors an"EU"topic outside the EU is rejected with400and the codeRESIDENCY_VIOLATION.retentionnames a tier for each age (step 3.4).tenantKeyis a KMS multi-Region key, so its replica in the paired region can decrypt mirrored data (step 3.3).
Legal hold
httpPOST /v1/legal-holds HTTP/1.1 Host: mq-control.internal Content-Type: application/json { "scope": { "tenant": "t_bank_eu_17", "topics": ["payments-eu"] }, "reason": "Litigation hold", "ticket": "LEGAL-2231" }
While a hold is active, retention and deletion skip the held data: segments aren't deleted, S3 objects get an S3 Object Lock legal hold (an object under a legal hold can't be deleted until the hold is removed), and any key that protects held data can't be destroyed.
Deletion requests
httpPOST /v1/deletion-requests HTTP/1.1 Host: mq-control.internal Content-Type: application/json { "tenant": "t_bank_eu_17", "customerId": "cust_88311", "ticket": "PRIV-5530" }
httpHTTP/1.1 202 Accepted Content-Type: application/json { "requestId": "del_7c1e", "status": "PENDING", "statusUrl": "/v1/deletion-requests/del_7c1e" }
202 means "accepted, in progress": deletion takes steps that finish at different times, and the status URL reports each one (step 3.3, trace 2 in R3.5). A request that touches data under a legal hold is recorded and waits, with status ON_HOLD.
Per-tenant encryption keys. Every tenant gets its own KMS key. Producers encrypt message bodies with data keys generated by KMS and encrypted under it, and topics that carry personal data also encrypt each customer's fields with a per-customer key (step 3.3).
R3.4 Design Evolution: Regions, Laws and Money
Step 3.1: A Region Is Gone
The problem: us-east-1 has a major outage. The payments topic lives there. Payments services fail over to us-west-2, and they need the last messages, and their consumers' positions, to be there. What would you do?
Synthesizing vector architecture diagram...
Nothing in us-east-1 waits for us-west-2. The copy trails the source by the mirror's lag, and the checkpoint topic lets a consumer group that moves to us-west-2 find its place in the copy.
Primitive: Cloud Disaster Recovery & Multi-Region Active-Active
Step 3.2: After Failover, Messages Arrived Twice and Out of Order
The problem: after the failover, payments in us-west-2 sees some messages twice. Worse, for account acct_42, a "refund" written in us-west-2 was processed before a "capture" that was written in us-east-1 just before the outage and arrived later, when us-east-1 came back.
What would you do?
Drill: Replication multi-region consistency
Step 3.3: EU Data Must Stay in the EU, and a Customer Wants Their Data Deleted
The problem: two requests on the same day. A regulator asks where an EU bank's payment messages are stored, including copies and backups. And one of that bank's customers asks for their data to be deleted, but their messages are spread across 90 days of shared, append-only segments in brokers, S3 and a mirror. What would you do?
The key table, and what the watch-list says about it:
| Attribute | Type | Example | Meaning |
|---|---|---|---|
customer_key_id (partition key) | String | t_bank_eu_17#cust_88311 | Tenant and customer |
wrapped_key | Binary | (32-byte key, encrypted under the tenant's KMS key) | The customer's data key |
home_region | String | eu-west-1 | The only region allowed to create this key |
created_at | String | 2026-09-26T12:00:00Z | When it was created |
- Create keys only in the key's home region, with a conditional put,
attribute_not_exists(customer_key_id), so two producers can't create two different keys for the same customer. - The table is a DynamoDB global table across the EU pair, so the paired region can decrypt after a failover. Global tables check a conditional write only against the local region's copy and, by default, settle conflicts by "last writer wins". If both regions could create a key for the same customer at once, one key would silently replace the other, and data encrypted under the loser would be unreadable forever. That's why creation happens only in the home region. (DynamoDB's multi-Region strong consistency mode would check conditions across Regions, but it runs in exactly three Regions (three replicas, or two replicas plus a witness), so we would need a third EU Region for a witness, and it doesn't support TTL. We choose not to add one, and create keys only in the home region instead.)
- Never use TTL to expire keys. DynamoDB TTL deletes items only eventually, typically within a few days, so it can't mark the moment a key stops working. Deletion is an explicit
DeleteItem, recorded in the deletion request's audit trail. - Backups hold keys too. A deleted row still exists in any backup of the table taken before the deletion. Shredding is complete only when the last such backup has expired, so the key table's backup retention is kept short and written down, and the deletion request's status only becomes
COMPLETEafter it has passed.
Step 3.4: 90-Day Retention Costs a Fortune
The problem: finance needs 90 days, and legal wants some data kept longer. Someone proposes simply setting retention to 90 days for every topic. Someone else proposes cutting everyone to 3 days to save money. What would you do?
The storage bill is in R3.6: with tiers and compression, about 600 TB stored for about $24,000 a month, instead of 13 PB for over a million.
Step 3.5: One Bad Config Took Down Every Topic
The problem: a configuration change lowered a broker memory setting for "all clusters". Every broker in a region restarted into the same crash within minutes. Every topic of every tenant in that region was down. What would you do?
Primitive: Circuit Breaker, Bulkhead & Fault Tolerance Patterns
Step 3.6: Should We Run This at All?
The problem: the CFO asks: "We're spending over $130,000 a month on this, plus the team that runs it. Amazon sells SQS, SNS, MSK and Kinesis Data Streams. Why are we running our own?" What would you do?
Round 3 Step Summary
| Step | Problem | Component | What it costs us |
|---|---|---|---|
| 3.1 | A region is gone | Async mirroring of critical topics to the paired region; offset syncs and checkpoints; per-topic target lag | Lag = data at risk; rereads after failover; cross-region bandwidth |
| 3.2 | Twice and out of order after failover | One home region per key; operator-switched with ARC routing controls; reconciliation topic for the late tail; idempotent consumers | Writes for a key wait for the switch |
| 3.3 | Residency and deletion | Residency-aware placement; per-tenant KMS keys (crypto-shredding); per-customer keys in a home-region key table; tombstones for keyed topics | Key management at scale |
| 3.4 | Retention cost | Tiers: brokers 24 h, S3 Standard 7 days, Glacier Instant Retrieval to 90 days; compression; per-topic policy | Slower, pricier cold replays |
| 3.5 | One config breaks everything | 18 cells; versioned cell map; waves with a canary cell | More clusters to run |
| 3.6 | Build or buy | SQS/SNS for task queues, MSK for streams; build only where needs are unmet | Lasting organizational cost |
R3.5 Global Architecture
Synthesizing vector architecture diagram...
Read it as two independent pairs. Inside each region, cells share nothing with each other. Critical topics flow one way, from their home region to its pair; EU data never leaves the EU pair. The control plane only pushes policies and the cell map, in waves, and ARC's routing controls decide which region of a pair is home. Mirroring is drawn one way for clarity: topics whose home is us-west-2 or eu-central-1 mirror in the other direction the same way.
Trace 1: us-east-1 fails, and payments moves to us-west-2.
Synthesizing vector architecture diagram...
Nothing switches automatically. An engineer confirms the outage and flips one routing control per topic group. Consumers find their place from the checkpoint and reread a few seconds; the last ~3 seconds of us-east-1 wait there until it returns.
Failing back. When us-east-1 is healthy, the mirror runs the other way (us-west-2 to us-east-1) until its lag is near zero. Then an operator switches the home back, consumers move using the checkpoints in us-east-1, and the unmirrored tail from the outage, now old, has already gone to reconciliation. Failback rereads a few seconds too.
Trace 2: a customer's deletion request.
Synthesizing vector architecture diagram...
No segment is rewritten anywhere. Once the customer's key row is gone, their encrypted fields in brokers, S3, Glacier and the mirror are unreadable; the request completes when no backup still holds the key.
R3.6 Numbers and Cost
Traffic per region. 20 billion messages a day is messages a second on average, and at peak. A planning split:
| Region | Messages/day | Own peak | Mirrored in at peak (25% of the pair's own peak) | Planned peak | Cells (81K each) | Load |
|---|---|---|---|---|---|---|
| us-east-1 | 6B | 243.1K/s | 40.5K/s | 283.6K/s | 5 (405K) | 70% |
| us-west-2 | 4B | 162.0K/s | 60.8K/s | 222.8K/s | 4 (324K) | 69% |
| eu-west-1 | 6B | 243.1K/s | 40.5K/s | 283.6K/s | 5 (405K) | 70% |
| eu-central-1 | 4B | 162.0K/s | 60.8K/s | 222.8K/s | 4 (324K) | 69% |
| Total | 20B | 810.2K/s | 18 cells, 216 brokers |
Each cell's 81K already includes Round 2's headroom for losing an AZ. The extra 30% covers growth and consumer surges. A region failure doesn't overload its pair: when us-east-1 fails, its critical producers (25% of 243.1K, so 60.8K a second) start writing to us-west-2, and the mirror stream they replace was exactly that size. The pair's planned peak stays at 222.8K.
Availability budget. 99.999% per region is seconds a month. A leader election takes 1 to 2 seconds, so the budget only survives because an election touches one broker's partitions, not the region, and the client library's retries hide it from callers. What would spend the budget is anything region-wide, which is why cells and waves matter: a bad change caught in one canary cell costs that cell's tenants, not the region.
Cross-region mirroring. 25% of the average rate is mirrored: messages a second, at 1 KB each after compression, which is about 57.9 MB/s, or 5 TB a day.
| Quantity | Math | Value |
|---|---|---|
| Busiest direction at peak | 60.8K/s × 1 KB | ≈ 60.8 MB/s ≈ 486 Mbps |
| Data at risk at 5 s of lag, busiest direction | 60.8K/s × 5 s | ≈ 304,000 messages |
| Mirrored bytes per month | 5 TB/day × 30.4 | ≈ 152 TB |
| Transfer cost | 152,000 GB × $0.02/GB | ≥ $3,040/month |
$0.02 per GB is the published rate between these US regions and between these EU regions at the time of writing; treat it as a floor and check the current price list for each pair.
Storage by tier (compressed, 1 KB per message; "own" is 20 TB/day, "mirrored" 5 TB/day, finance is 20% of own, 4 TB/day):
| Tier | What's in it | Size | Math | Monthly |
|---|---|---|---|---|
| Broker disks (EBS gp3) | 24 h of own + mirrored, 3 replicas, 20% overhead | 90 TB used, 216 TB provisioned | 216 × 1 TB × $0.08, + 125 MB/s each × $0.04 | ≈ $18,360 |
| S3 Standard | 7 days of own + mirrored, one copy | TB | 175,000 GB × $0.023 | ≈ $4,025 |
| S3 Glacier Instant Retrieval | finance, days 8 to 90 | TB | every GB billed 90 days: 121,600 GB/month × $0.004 × 90 ÷ 30.4 | ≈ $1,440 |
| S3 requests and lifecycle transitions | hourly segments in 18 cells | ≈ $400 | ||
| Total | ≈ 600 TB | ≈ $24,200 |
The S3 Standard figure is an upper bound: past the first 50 TB a month, the price per GB steps down. For comparison: all 90 days on brokers, uncompressed, is about 13 PB and over $1 million a month (step 3.4).
Fleet, all four regions. 18 cells × 12 brokers = 216 m7g.2xlarge, and 54 controllers (3 per cell) on m7g.large. The broker disks are 42% full; the rest leaves more than a day of room if S3 uploads fail.
Cross-AZ traffic inside cells. All four regions together write TB a day into their cells, so 760 TB a month.
| Flow | Math | Monthly |
|---|---|---|
| Replication to 2 followers | 2 × 760 TB × $0.02/GB | ≈ $30,400 |
| Producers and mirror workers to leaders in other AZs | 2/3 × 760 TB × $0.02/GB | ≈ $10,133 |
| Consumers (fetch from the same AZ) | ≈ $0 |
Monthly total (us-east-1 prices everywhere; the EU regions cost somewhat more, so this is a floor):
| Item | Math | Monthly |
|---|---|---|
| 216 brokers | 216 × $0.3264/h × 730 h | ≈ $51,467 |
| 54 controllers | 54 × $0.0816/h × 730 h | ≈ $3,217 |
| Storage, all tiers | table above | ≈ $24,225 |
| Cross-AZ | table above | ≈ $40,533 |
| Cross-region mirroring | table above | ≥ $3,040 |
| Mirror workers | 4 directions × 6 × m7g.xlarge × $0.1632/h × 730 h | ≈ $2,859 |
| KMS | ~2,000 tenant keys + ~500 replica keys × $1, plus requests | ≈ $2,600 |
| DynamoDB customer key table | a few GB, two EU regions, reads cached | ≈ $100 |
| ARC routing control cluster | $2.50/h × 730 h | ≈ $1,825 |
| CloudWatch metrics, alarms, logs | ≈ $2,000 | |
| Total | ≈ $132,000/month |
Cross-AZ traffic ($40.5K) is almost as big as the brokers ($51.5K), and three quarters of it is replication.
The same shape on Amazon MSK.
| Item | Math | Monthly |
|---|---|---|
216 MSK brokers (kafka.m7g.2xlarge) | 216 × $0.816/h × 730 h | ≈ $128,670 |
| MSK broker storage | 216,000 GB × $0.10 | ≈ $21,600 |
| MSK tiered storage, 7 days | 175,000 GB × $0.06 | ≈ $10,500 |
| Finance archive exported to our own Glacier IR | as above | ≈ $1,840 |
| Replication between brokers | not charged by MSK | $0 |
| Producers and mirror workers across AZs | as above | ≈ $10,133 |
| Mirroring, KMS, key table, ARC, CloudWatch | as above | ≈ $12,424 |
| Total | ≈ $185,000/month |
MSK's broker hour costs 2.5 times the EC2 instance's; its free replication traffic wins back about $30K of that. Net, building saves about $53K a month at this scale, the input to step 3.6's decision. MSK also offers Express brokers, priced and sized differently, which could need fewer brokers for the same throughput; we'd price that option too before deciding.
R3.7 Trade-Offs
| Choice | We chose | What we give up |
|---|---|---|
| Mirroring: sync vs async | Async, per topic, with a target lag | Up to ~5 s of critical messages lost in a region failure. Sync would add a cross-region round trip (tens of ms) to every send. |
| Home-region keys vs multi-writer | One home region per key, switched by an operator | Writes for a key pause during its home's outage until the switch. Multi-writer keeps writing, but order per key and dedup no longer mean anything. |
| Crypto-shredding vs rewriting logs | Crypto-shredding: per-tenant KMS keys and per-customer keys | Key management, and a mistaken key deletion destroys data for real. Rewriting logs would read and rewrite terabytes per request, in every copy. |
| Cells vs one cluster per region | 18 cells | 18 clusters' worth of operations, and tenants bigger than a cell must split. One cluster is simpler, and one mistake reaches everyone. |
| Retention tiers vs one tier | Brokers 24 h, S3 Standard 7 d, Glacier IR to 90 d | Cold replays cost $0.03/GB and are slower. One tier is simpler and ~40 times more expensive at 90 days. |
| Build vs buy | Buy for most: SQS/SNS for queues, MSK for streams | Their limits and prices, and a migration. Building saves ~$53K a month but needs a team that costs more. |
Closing the loop. The opening question was: how do we make sure every message is delivered, none are lost, and order holds where we promised it, while machines and consumers fail? The answer is now layered:
- Delivered: leases with a visibility timeout and fenced receipt handles, at least once, with idempotent consumers, in every round.
- Never lost: committed on a majority of replicas in three AZs before we say "got it"; mirrored to a second region for critical topics, where "never" becomes "at most about 5 seconds", a number the business chose.
- Ordered where promised: per key, in one partition, in one home region, and switched between regions only by a deliberate act.
And the architect's addition: for most workloads, the best way to keep those promises is to buy them from a service that already does, and spend our effort on the policies around them.
R3.8 Failure Modes
| Failure | What you'd see | How the design responds |
|---|---|---|
| A region outage | Every cell in the region unhealthy; mirror lag into the pair frozen | Non-critical topics wait for the region. For critical topics, the on-call engineer confirms the outage and flips the home with ARC; consumers resume from checkpoints (trace 1). |
| A mirroring lag spike | Mirror lag above 5 s on some topics | Usually a burst or a slow target cell. Mirror workers scale out; the source is never slowed down to wait for them. While lag is high, a region failure would lose more, so the alarm pages. |
| Failback duplicates | A few seconds of reprocessing when consumers move back | Expected: checkpoints round down. Consumers are idempotent. We fail back only once the reverse mirror's lag is near zero. |
| A KMS outage | Data-key requests failing in one region | Producers keep using their cached data key (valid up to an hour) and consumers their cached decrypted keys, so live traffic continues. New producers and consumers of new batches can't start until KMS recovers. We alarm on KMS errors long before caches run out. |
| A cold-tier read storm | Many teams replaying months of finance data at once; Glacier IR retrieval charges climbing | Replays from cold tiers have their own quota per team and a budget alarm. Each GB retrieved costs $0.03, so a 100 TB storm is a $3,000 bill, and quotas keep it from being much bigger. |
| A cell outage | One cell's alarms, only its tenants affected | That's the point of cells: the other 17 carry on. The wave that caused it, if any, stops and rolls back. Tenants in that cell are down until it recovers; critical topics can fail over to their pair region like in a region failure. |
R3.9 Runbook and Incident Response
Golden signals, per cell and per region OPS 8 · REL 6
| Signal | Alarm | Severity | First action |
|---|---|---|---|
| Queue backlog (visible messages) per queue topic | Above the queue's own normal range for 15 min | P3 | Are the workers healthy and scaled? Is their downstream slow? |
| Oldest message age per queue topic | > 5 min | P2 | Same, and check for a blocked FIFO group |
| Consumer lag per group (seconds behind) | Above the group's target for 10 min | P3 | Rebalances, errors, or a slow downstream |
| Mirroring lag per critical topic | > 5 s for 2 min | P1 | Scale mirror workers; check the target cell and cross-region network |
| DLQ inflow | > 0 in 5 min | P2 | Page the owning team; look at the first message's error |
| Under-replicated partitions per cell | any for 5 min | P2 | Find the lagging broker |
| KMS errors per region | > 1% of calls for 5 min | P2 | Check KMS status; caches give about an hour |
| Cold-tier retrieval spend per team | over its daily budget | P3 | Ask the team; pause the replay if it's a mistake |
DLQ redrive procedure OPS 10
- Look before moving. Read the first few DLQ messages and their error attributes. Find the one cause; don't redrive a mix of causes.
- Fix the cause. Deploy the consumer fix, or correct the bad data at its source.
- Test with one. Redrive a single message and watch it succeed.
- Redrive at a limited rate, well under what the consumers handle, so old messages don't swamp live traffic. For an ordered queue, redrive a group's messages in order and before that group's newer messages are released, or send them to reconciliation instead.
- Watch the DLQ. If messages come back, stop and return to step 1.
- Close with a note of the cause and the fix, and an alarm or test that would have caught it earlier.
Region evacuation procedure REL 13
- Confirm it's the region. Several cells unhealthy in one region at once, and AWS Health or our own cross-region probes agree. One cell down is a cell incident, not a region one.
- Check what we'd lose. Note each critical topic's last mirror lag: that's the data that stays behind.
- Flip the home for each critical topic group with its ARC routing control, one group at a time, payments first.
- Watch consumers resume from checkpoints in the paired region; expect a few seconds of rereads.
- Protect the pair. Confirm it's under its planned peak; pause any wave deploys in it.
- When the region returns: send its unmirrored tail to reconciliation topics, run the mirror in reverse until its lag is near zero, then fail back the same way, and write the COE (Correction of Error).
Go deeper: CLI playbook
Plain commands an on-call engineer runs, one at a time. Replace the ARNs, names and endpoints with real ones.
text# 1. Alarms currently firing for the platform in one region aws cloudwatch describe-alarms --region us-east-1 --state-value ALARM --alarm-name-prefix mq- # 2. Mirroring lag for one critical topic over the last hour (a custom metric the mirror workers publish) aws cloudwatch get-metric-statistics --region us-west-2 --namespace MQ/Mirror --metric-name LagSeconds --dimensions Name=Topic,Value=payments --start-time 2026-09-26T11:00:00Z --end-time 2026-09-26T12:00:00Z --period 60 --statistics Maximum # 3. Which region is home for payments right now (ask an ARC cluster endpoint in a healthy region) aws route53-recovery-cluster get-routing-control-state --region us-west-2 --endpoint-url https://abcd1234.route53-recovery-cluster.us-west-2.amazonaws.com/v1 --routing-control-arn arn:aws:route53-recovery-control::111122223333:controlpanel/0123456789abcdef/routingcontrol/payments-use1 # 4. Evacuate: turn us-east-1 off and us-west-2 on as home for payments aws route53-recovery-cluster update-routing-control-state --region us-west-2 --endpoint-url https://abcd1234.route53-recovery-cluster.us-west-2.amazonaws.com/v1 --routing-control-arn arn:aws:route53-recovery-control::111122223333:controlpanel/0123456789abcdef/routingcontrol/payments-use1 --routing-control-state Off aws route53-recovery-cluster update-routing-control-state --region us-west-2 --endpoint-url https://abcd1234.route53-recovery-cluster.us-west-2.amazonaws.com/v1 --routing-control-arn arn:aws:route53-recovery-control::111122223333:controlpanel/0123456789abcdef/routingcontrol/payments-usw2 --routing-control-state On # 5. Put one archived segment under legal hold, and check it (the bucket must have Object Lock enabled) aws s3api put-object-legal-hold --bucket mq-archive-eu-west-1 --key payments-eu/p17/00000000000005520000.log --legal-hold Status=ON aws s3api get-object-legal-hold --bucket mq-archive-eu-west-1 --key payments-eu/p17/00000000000005520000.log # 6. A tenant leaves: check its key, then schedule deletion with the longest waiting period (replica keys too) aws kms describe-key --region eu-west-1 --key-id mrk-1234abcd12ab34cd56ef1234567890ab aws kms schedule-key-deletion --region eu-central-1 --key-id mrk-1234abcd12ab34cd56ef1234567890ab --pending-window-in-days 30 aws kms schedule-key-deletion --region eu-west-1 --key-id mrk-1234abcd12ab34cd56ef1234567890ab --pending-window-in-days 30 # 7. Undo a mistaken deletion during the waiting period aws kms cancel-key-deletion --region eu-west-1 --key-id mrk-1234abcd12ab34cd56ef1234567890ab # 8. For task queues already moved to SQS (step 3.6): redrive a DLQ back to its source at 50 messages a second aws sqs start-message-move-task --source-arn arn:aws:sqs:us-east-1:111122223333:order-fulfillment-dlq --destination-arn arn:aws:sqs:us-east-1:111122223333:order-fulfillment --max-number-of-messages-per-second 50 aws sqs list-message-move-tasks --source-arn arn:aws:sqs:us-east-1:111122223333:order-fulfillment-dlq
Command 4 asks a cluster endpoint in us-west-2 on purpose: during a us-east-1 outage, we use ARC's endpoints in the regions that are still up. A key scheduled for deletion is unusable for the whole waiting period, so step 6 is only for a tenant whose offboarding has been approved, after checking legal holds.
R3.10 Pillar Check
| Pillar | What Round 3 adds |
|---|---|
| Reliability | Region pairs with async mirroring and an RPO chosen per topic; offset translation that never skips; one home region per key, switched deliberately with ARC; cells so one mistake reaches one cell; an evacuation and failback procedure REL 13 · REL 10 · REL 11 · REL 8 |
| Performance Efficiency | No cross-region wait on any send; consumers read in their own AZ; replays from the cold tier on their own path and quota PERF 1 · PERF 3 · PERF 4 |
| Security | Data classified by tenant and residency, and placement enforced by policy; per-tenant KMS keys with envelope encryption; per-customer keys for personal data, so deletion is a key deletion; legal holds with S3 Object Lock; mirrored traffic between regions over TLS SEC 7 · SEC 8 · SEC 9 |
| Cost Optimization | About $132K/month, derived by line; storage tiers turn 13 PB and $1M+/month into 600 TB and $24K; compression before mirroring; the MSK comparison, and a build-or-buy decision that counts the team COST 4 · COST 8 · COST 11 |
| Operational Excellence | Golden signals per cell with first actions; DLQ redrive and region evacuation procedures; wave deploys through a canary cell; COEs after each incident OPS 6 · OPS 7 · OPS 10 · OPS 11 |
| Sustainability | Retention per topic, so most data lives 7 days and only finance's lives 90; compression halves every byte stored and sent; one stored copy serves every consumer group; Graviton brokers SUS 4 · SUS 5 · SUS 2 |
R3.11 Round 3 Rubric and Follow-Ups
What an architect (L7) answer adds over L6
- Chooses async mirroring with an RPO set per topic, and can say how many messages that RPO means at peak.
- Knows that offsets, order and dedup stop at a region's edge, and designs each back in: offset translation, home regions, idempotent consumers.
- Treats failover as a deliberate, fenced decision rather than an automatic reaction to a health check.
- Designs for law and policy without overclaiming: residency enforced by placement, deletion by crypto-shredding, legal holds that beat both.
- Finds the real costs (cross-AZ replication, retention) and uses tiers and minimum-duration rules to cut them.
- Limits blast radius with cells and waves.
- Answers "should we run this?" with numbers, including the team, and is willing to recommend buying.
Follow-up questions
-
"Why not active-active: let both regions of a pair accept writes for every topic?" Answer: for unordered topics with idempotent consumers, we can: each region writes its own messages, mirrors them to the other, and consumers read both, tagged by origin so the mirror never copies a message back. For ordered topics it breaks the promise: two regions accepting writes for
acct_42can't agree on their order without a cross-region round trip on every write. So ordered topics have a home region, and unordered ones may be multi-writer by policy. -
"A customer's deletion request arrives while their data is under legal hold. What does the system do?" Answer: the system doesn't decide which wins; it makes both visible and enforces whatever legal decides. The request is recorded with status
ON_HOLD, the customer's key can't be deleted while a hold covers it, and the privacy team sees the conflict. When legal releases the hold, the deletion proceeds. Every step is in an audit log. -
"Some newer Kafka-compatible systems write straight to S3 and keep no replicas on broker disks. Would that fit here?" Answer: for some topics, yes. S3 stores data redundantly across AZs itself, so there's no replication traffic between brokers, which is our biggest cost line ($30K a month). The price is latency: every write waits for an S3 upload, tens of milliseconds or more, so a 15 ms P99 is out of reach. It's a good fit for analytics and log topics that can wait a second, and a bad one for payments. A platform could offer it as a third kind of topic, with its own price and its own latency promise.
Loop Closer: Interview Strategy for All Three Rounds
How to Run Each 60-Minute Round
| Time | Round 1 | Round 2 | Round 3 |
|---|---|---|---|
| 0–5 min | Scoping: order? duplicates OK? size? how long to keep? | Restate the Round 1 design in 60 seconds | Restate the Round 2 design in 60 seconds |
| 5–15 min | Requirements + API (receipt handles, visibility) | Scope raise → what breaks | Scope raise → what breaks |
| 15–40 min | Steps 1.0–1.6: list → log → lease → Raft → partitions → long polling → DLQ | Steps 2.1–2.7: order per key, dedup, queue vs log, tiered storage, fenced handles, rebalancing, hot keys | Steps 3.1–3.6: mirroring and offsets, home regions, residency and shredding, tiers, cells, build vs buy |
| 40–50 min | Numbers + trade-offs, and "we'd buy SQS" | Numbers, cost (cross-AZ!), trade-offs | Numbers, cost by tier, MSK comparison, trade-offs |
| 50–60 min | Failures + pillar check | Failures, gotchas, pillar check | Failures, runbook, pillar check |
For how to spend a single 45-minute round, see the 45-minute interview blueprint. The other loops in this track: Distributed Key-Value Store and Distributed Unique ID Generator.
The Two Sentences That Matter Most
- Opening a round: "Before I design, let me ask two things: does order matter, and can a message be processed twice if the consumer is idempotent?"
- When the scope is raised: "Here's what breaks in the current design, and here's the order I'll fix it in, starting with anything that could lose an acknowledged message."
Well-Architected Review Sheet
Interviewers rarely ask "which pillar is this?". They ask the pillar's question in plain words. Rehearse one sentence per row.
| Pillar | Question you'll hear | One-sentence answer | Round | Backed by |
|---|---|---|---|---|
| Reliability | "What happens when a worker dies mid-task?" (REL 11) | Its lease expires and the message goes to another worker; idempotent workers make the repeat harmless. | 1 | Step 1.2 |
| "What happens when a broker's disk dies?" (REL 11) | Nothing we acknowledged is lost: every message was on 2 of 3 replicas in different AZs before we said "got it". | 1 | Step 1.3 | |
| "How do you stop duplicates?" (REL 4) | An idempotent producer and a 5-minute dedup window at the door, and idempotent consumers behind it, since processing is still at-least-once. | 2 | Step 2.2 | |
| "What happens when an AZ goes down?" (REL 10) | Every partition keeps a majority, elects a leader in the other two AZs, and the 8 remaining brokers run at about 30% of their network. | 2 | R2.6, R2.8 | |
| "What happens when a region goes down?" (REL 13) | Critical topics are already mirrored; an operator flips their home region, and consumers resume from translated offsets, losing at most ~5 seconds. | 3 | Steps 3.1–3.2 | |
| Performance | "How do you keep latency low?" (PERF 3) | Sequential appends with group commit, the page cache for live readers, long polling, and a budget we can show. | 1–2 | R1.7, R2.6 |
| "What if one reader replays a week of data?" (PERF 3) | It reads from S3 on its own path, so it can't push live readers' data out of memory. | 2 | Step 2.4 | |
| Cost | "What does it cost?" (COST 5) | About $810, $12.8K and $132K a month; cross-AZ replication is the biggest surprise. | 1–3 | R1.7, R2.6, R3.6 |
| "Should we run this ourselves?" (COST 11) | Usually not: SQS for task queues and MSK for streams, unless the savings beat the team's cost. | 1–3 | R1.8, R2.6, Step 3.6 | |
| "How do you pay for 90-day retention?" (COST 4) | Tiers by age and by topic, compression, and Glacier Instant Retrieval from day 8. | 3 | Step 3.4 | |
| Operations | "How do you know it's healthy?" (OPS 8) | Backlog, oldest-message age, consumer lag, mirror lag and DLQ inflow, each with a first action. | 1–3 | R2.10, R3.9 |
| "How do you ship changes safely?" (OPS 6) | In waves, one canary cell first, stopping on any alarm. | 3 | Step 3.5 | |
| Security | "Who can read a topic?" (SEC 3) | Only groups granted it, over mutual TLS with a certificate per service. | 1–2 | R1.10, R2.10 |
| "How do you delete one customer's data?" (SEC 8) | Their fields are encrypted with their own key; we delete the key, unless legal holds the data. | 3 | Step 3.3 | |
| Sustainability | "Where's the waste?" (SUS 4) | Data kept longer than anyone needs, and copies per team; retention per topic and one copy for all groups remove both. | 2–3 | Steps 2.3, 3.4 |
Rubric Across Levels
| Dimension | L5 (Round 1) | L6 (Round 2) | L7 (Round 3) |
|---|---|---|---|
| Delivery semantics | At-least-once with visibility-timeout leases; explains why exactly-once isn't free. | Idempotent producer, dedup window, idempotency keys; knows where Kafka's exactly-once stops. | Duplicates and order across regions; idempotency that survives a failover. |
| Durability | Log, fsync with group commit, Raft majority across AZs, and how a leader is elected. | One journal per broker at hundreds of partitions; knows the replicate-then-flush trade. | Async mirroring with a chosen RPO, and what it means in messages. |
| Queue vs log | Builds a queue. | Explains why logs exist, and runs both contracts. | Chooses per workload which managed service fits each. |
| Order | Knows partitions drop order and that's fine here. | FIFO per key; head-of-line blocking; hot groups; fixed partition counts. | One home region per key, and a deliberate switch. |
| Data and law | Retention of 4 days. | 7 days with tiered storage. | Residency by placement, crypto-shredding, legal holds, tiers with minimum durations. |
| Well-Architected trade-offs | Honest build vs SQS. | Sizes for AZ loss; finds cross-AZ traffic as the top cost; MSK comparison. | Cells and waves; a costed build-or-buy that includes the team. |
| Evolving under new scope | Builds from one list, one failure at a time. | Opens with "what breaks", fixes order and duplicates first, then fan-out and replay. | Evolves the platform and its rules: regions, laws, money, and what not to build. |