Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.
What Does It Take to Never Lose an "Add to Cart"?
It is the first morning of Prime Day, 23 June 2026, in Seattle. Ishaan Mehta, a second-year master's student, has had the same cart for a week: a 27-inch monitor, a mechanical keyboard, and a phone case he has already decided against. On the 8:10 bus he finds a deal on noise-cancelling headphones and adds them from his phone. The bus goes under the freeway, his signal drops to one bar, and the app shows "Added to Cart" anyway.
At home, his laptop still has yesterday's cart open in a browser tab. He removes the phone case there, reviews the order, and presses "Place your order". The page spins for nine seconds. He presses it again. Somewhere behind those two presses, a cart written from two devices has to come out as exactly the three items he wants, the headphones have to be reserved before the deal's stock runs out, his card has to be charged once, and a warehouse in a different state has to learn about one order, not two. Millions of other people are doing something similar in the same minute.
- 192 million requests a second at peak and 59 trillion+ requests from 23 to 26 June to DynamoDB, which "powers multiple high-traffic Amazon properties and systems including Alexa, the Amazon.com sites, and all Amazon fulfillment centers" (AWS News Blog, Prime Day 2026, 6 October 2026)
- 12.9 → 45.4 → 89.2 → 105.2 → 126 → 146 → 151 → 192 million requests a second: DynamoDB's Prime Day peaks in 2017, 2019, 2021, 2022, 2023, 2024, 2025 and 2026 (AWS News Blog posts; the DynamoDB paper, USENIX ATC 2022, for 2021)
- 213 million messages a second at peak through Amazon SQS on Prime Day 2026; 166 million in 2025 (AWS News Blog)
- 100,000+ items purchased a minute during Prime Day 2022 (Amazon, as quoted on the AWS News Blog, July 2022)
- 150+ services called to build one page, in 2007, with Amazon's service SLAs expressed and measured at the 99.9th percentile (Dynamo paper, SOSP 2007)
- 3 million+ checkouts in a single holiday-season day from a cart service that served "tens of millions" of requests, in Dynamo's first year (Dynamo paper, 2007)
For context:
The DynamoDB, SQS and fault-injection numbers are totals for all of Amazon's systems on AWS (Alexa, the shopping sites, fulfilment centres), not for the cart alone
Amazon has published how its cart storage worked in 2007 (the Dynamo paper), how DynamoDB works today, and the patterns its engineers use (the Amazon Builders' Library, AWS's cell-based architecture guidance). It has not published how today's cart, checkout or order pipeline are built, so those parts are labelled as our design
Every number labelled as an assumption is illustrative
This is the system we'll design today.
Over Prime Day 2026 Amazon's systems made 59 trillion+ requests to DynamoDB, peaking at 192 million a second, and in 2022 shoppers bought 100,000+ items a minute; those are platform totals. On stated assumptions the checkout peak is ~5,000 orders and ~100,000 cart operations a second (designed for 20,000 and 400,000), and one deal's stock counter would take ~5,000 writes a second. The cart is availability-first: a conditional write on the cart's version, an add-log in the same cell when that partition is slow, and lines with unique tags so a merge keeps every add and a remove stays removed (Dynamo's 2007 union let removed items resurface). The order is correctness-first: the checkout page mints a purchase id, and the token, the order and an outbox row commit in one transaction, so a double press returns the same order and the same id with a different cart is rejected as a mismatch. Stock is reserved at placement, a hot deal split into sub-counters of 1,000 that only decrement while positive. Customers live in ~40 cells with cell-by-cell deploys, and the shared price service gives each cell its own 4 of 32 server groups, so a bad build or a poison request reaches about 2.5% of shoppers. Behind the page an orchestrator authorises payment (a timeout is unknown, retried with the same key) and releases one shipment per fulfilment centre, draining any backlog fresh-first. The edge cases are a guest cart, a price change, bots, a lost zone and Prime Day 2027 with a zone down.
The Interview Begins#
You're interviewing at a large e-commerce company. The interviewer shares two screenshots from the same morning: a cart on a phone reading "Subtotal (4 items)", and an order confirmation on a laptop reading "Order placed, thank you! · 3 items · Arriving Thursday".
"Four items on one screen, three on the other, and the second one is right. Between them are two devices, two presses of the button, a deal with limited stock, a card payment and a warehouse. Design the cart, checkout and order pipeline for the biggest sale days of the year."
A store with hundreds of millions of customers runs a multi-day sale once or twice a year. Customers add and remove items on several devices, often with poor connections. At checkout they pick an address, a delivery option and a payment method, and place the order. Some deals have a fixed number of units. Behind the order, stock is reserved in a fulfilment centre, the payment is authorised, and the order is released to the warehouse.
Requirements
- A cart that accepts adds and removes from any device, always, even during failures
- Checkout that re-prices the cart and shows the true total before the order is placed
- Place the order exactly once, however often the request is retried
- Reserve stock without overselling, including limited-quantity deals
- Authorise payment and hand each order to fulfilment exactly once
- Survive the sale's peak, a bad deploy or a lost data centre without a global outage
"Search, recommendations, the product page and the delivery network are other teams' problems. You own everything from 'Add to Cart' to the moment the warehouse has the order."
Phase 1: Requirements Clarification (5 minutes)#
"Two things decide most of the design: what the cart is allowed to get wrong, and what the order is never allowed to get wrong. I'll ask about those first, then about scale."
Your Questions#
"Can the cart ever refuse a write?"
"No. Amazon's Dynamo paper put it this way in 2007: customers 'should be able to view and add items to their shopping cart even if disks are failing, network routes are flapping, or data centers are being destroyed by tornados', and an 'Add to Cart' operation 'can never be forgotten or rejected'."
"Then the cart may sometimes show a slightly stale version, or bring back an item someone removed?"
"The paper accepted exactly that: when versions of a cart diverge, they are merged, and 'an add to cart operation is never lost. However, deleted items can resurface.' Tell me whether you'd accept it today."
"And the order?"
"The opposite. One press, one order, one charge. If the customer presses twice, or the app retries after a timeout, there is still one order."
"How big are the peaks?"
"AWS publishes Prime Day numbers every year. In 2026 the sale ran from 23 to 26 June; Amazon's systems made more than 59 trillion requests to DynamoDB over those days, peaking at 192 million a second, and SQS peaked at 213 million messages a second. Those are all of Amazon's systems, not just the cart. For orders, the last purchase rate Amazon gave in those posts was more than 100,000 items a minute during Prime Day 2022."
"And latency?"
"In 2007 a page called more than 150 services, and Amazon expressed and measured its service SLAs at the 99.9th percentile; the paper's example is 'a response within 300ms for 99.9% of its requests for a peak client load of 500 requests per second'. Assume that discipline."
"Is there a deal with limited stock?"
"Yes. Some deals have a fixed number of units. Selling one more than exists is a cancellation and an apology."
Functional Requirements#
- Cart: add, remove and change quantity from any device; signed-in carts follow the customer; a guest cart merges into the account cart at sign-in; the cart never refuses an add
- Checkout: re-price every line with current prices and promotions, check availability, pick address, delivery speed and payment, and show the final total
- Place order: exactly once per intent; a retried or double-pressed request returns the same order
- Reserve stock: take units from a specific fulfilment centre when the order is placed; never sell a limited deal past its quantity
- Payment: authorise when the order is placed, charge later (our design); a declined card leaves the order waiting for a new method, not lost
- Hand-off: release every confirmed order to fulfilment exactly once; cancel and release stock cleanly when an order can't complete
Non-Functional Requirements#
Availability
- Cart writes: accepted during node, network and data-centre failures (the Dynamo requirement)
- Order placement: a failure in one part of the fleet affects a small, bounded share of customers
Latency (design targets)
- Cart add p99.9 < 300 ms, the Dynamo-era target
- Place order p99.9 < 2 s, including stock reservation
Correctness
- Zero duplicate orders or charges from retries
- Zero oversold limited-quantity deals
- Zero confirmed orders that never reach fulfilment
Phase 2: Back of the Envelope Estimation (5 minutes)#
"Two published anchors for the shopping side: 100,000 items a minute in 2022, and the Dynamo paper's ratio of 'tens of millions' of cart requests to 'well over 3 million' checkouts in a day. The platform-wide DynamoDB and SQS peaks tell us the scale of the infrastructure, not of our service. Everything else is my assumption, labelled."
Before the numbers: what does one page cost?
In 2007 one page called more than 150 services, and Amazon measured its service SLAs at the 99.9th percentile. If each call independently has a 0.1% chance of being slower than its target, what fraction of pages has at least one slow call? What does that tell you about where to set the targets? And the Dynamo paper's cart served "tens of millions" of requests for "well over 3 million" checkouts in a day: roughly how many cart requests per checkout is that?
One strong answer
- Slow pages: 1 − 0.999¹⁵⁰ ≈ 1 − 0.86 = ~14% of pages hit at least one call in its slowest 0.1%. With a target at the median, nearly every page would.
- So each dependency's target must sit far out in its tail (p99.9), and the page must not wait for optional parts: a slow recommendation widget is dropped, not waited for. This is why Amazon measured at the 99.9th percentile rather than the mean.
- Cart requests per checkout: "tens of millions" ÷ "over 3 million" is at least ~10 cart requests per checkout, before the browsing that never ends in a checkout.
Orders and Cart Traffic#
| Value | |
|---|---|
| Items purchased, Prime Day 2022 (Amazon, published) | 100,000+ a minute ≈ 1,700 a second |
| Growth to 2026 (assumed) | ×1.5 → ~2,500 items a second on average |
| Items per order (assumed) | ~2 → ~1,250 orders a second on average |
| Peak hour over average (assumed: deal drops, time zones) | ×4 → ~5,000 orders a second |
| Design target for order placement (×4 headroom) | 20,000 orders a second |
| Cart reads and writes per order (Dynamo ratio ≥10, assumed 20 with browsing) | ~100,000 a second at peak |
| Design target for cart operations | 400,000 a second |
Conclusion: order placement is tens of thousands a second at most; cart traffic is an order of magnitude more. Neither is large next to the platform's 192 million database requests a second, so the problem isn't raw throughput. It's the tail latency, the failure modes, and the few keys that everyone touches at once.
The Hot Spots#
- Deal: 20,000 units (assumed) of one pair of headphones, live at a fixed time
- Demand: 150,000 customers try to buy in the first 2 minutes (assumed) → ~1,250 attempts a second, most in the first 20 seconds → ~5,000 a second at the start
- One stock counter is one item on one partition. The DynamoDB paper's worked example assumes a partition can take a maximum of 1,000 write units a second, and splitting a partition does not help "a partition receiving high traffic to a single item"
- So the deal's stock is split across ~20 counters (Deep Dive 5), and each takes ~250 writes a second at the peak
Conclusion: the deal is a hot-key problem in an otherwise cool keyspace; fix it in the data model, not with a bigger database.
Key Metrics Summary#
| Metric | Value |
|---|---|
| DynamoDB peak, all of Amazon, Prime Day 2026 (published) | 192 million requests a second |
| SQS peak, all of Amazon, Prime Day 2026 (published) | 213 million messages a second |
| Items purchased, Prime Day 2022 (published) | 100,000+ a minute |
| Orders at peak / design target (assumed) | ~5,000 / 20,000 a second |
| Cart operations at peak / design target (assumed) | ~100,000 / 400,000 a second |
| Cart latency target (Dynamo-era SLA) | 300 ms at p99.9 |
| Services per page (2007, published) | 150+ |
| Hottest deal counter, unsharded (assumed) | ~5,000 writes a second |
Requirements and estimation
Q1/3 A page calls 150 services, each slow 0.1% of the time. Roughly what share of pages hits at least one slow call?
Answer & explanation
Answer: C
1 − 0.999^150 ≈ 0.14. That's why each service's target sits at p99.9, and optional parts of a page are dropped rather than waited for.
Q2/3 The Dynamo paper reports "tens of millions" of cart requests for "well over 3 million" checkouts in a day. What does that ratio tell you?
Answer & explanation
Answer: B
Cart traffic dominates; order placement is the smaller, stricter flow.
Q3/3 Why is the published 192 million requests a second not your cart's load?
Answer & explanation
Answer: B
AWS's posts give platform totals. Use them for context; size the service from orders and cart operations.
Phase 3: High-Level Design (10 minutes)#
Sketch it first: Ishaan's two presses
Ishaan's phone added headphones during a signal drop; his laptop removed the phone case from an older view of the cart. Which service owns the cart, and how does it end up with three items? Then he presses "Place your order" twice. What makes the second press return the first order? Where is the headphones' stock taken, and when? What talks to his bank, and does the page wait for it? And if the servers that hold Ishaan's cart are hit by a bad deploy, who else is affected?
One strong answer
- Cart service stores the cart as one key per customer, with each line as its own entry; adds and removes are separate operations, so the add from the phone and the remove from the laptop both survive (Deep Dive 1).
- The checkout page creates a purchase id when it loads; both presses carry it, and order placement records the id and the order in one atomic write. The second press finds the id and returns the same order (Deep Dive 4).
- Stock is reserved synchronously at placement from a specific fulfilment centre's counter, so "sold out" shows now, not in an email later (Deep Dive 5).
- Payment authorisation happens in the order pipeline after the order is accepted; the page doesn't wait for the bank (Deep Dive 6).
- Blast radius: customers are split into cells, each a full copy of cart, checkout and order services with its own tables. A bad deploy in Ishaan's cell affects only that cell's share of customers (Deep Dive 3).
"Three ideas carry the design. The cart is availability-first: it never refuses a write and reconciles later. The order is correctness-first: one intent, one order, decided by an atomic write. And everything customer-scoped lives in cells, so no single failure reaches every customer."
System Architecture#
%% title: Cart, checkout and orders, in cells, on a shared catalogue and stock
flowchart TD
subgraph L1["Layer 1 · Customers"]
direction LR
APP@{ icon: "sd:phone", label: "Shopping app<br/>cart, checkout" }
WEB@{ icon: "sd:browser", label: "Website<br/>same APIs" }
end
subgraph L2["Layer 2 · Edge"]
direction LR
CDN@{ icon: "sd:cdn", label: "CDN<br/>images, scripts" }
GW@{ icon: "sd:gateway", label: "Front door<br/>auth, priority, shedding" }
RT@{ icon: "sd:router", label: "Cell router<br/>customer id to cell" }
end
subgraph L3["Layer 3 · One cell of many"]
direction LR
CART["Cart service<br/>always writable"]:::core
CHK["Checkout and order<br/>idempotent placement"]:::hot
CDB@{ icon: "sd:database", label: "Cell tables<br/>carts, orders, tokens" }
end
subgraph L4["Layer 4 · Shared, shuffle-sharded"]
direction LR
PRICE@{ icon: "sd:cache", label: "Price and promo<br/>read-mostly" }
INV["Inventory<br/>per product and FC"]:::core
end
subgraph L5["Layer 5 · After the order"]
direction LR
Q@{ icon: "sd:queue", label: "Order queues<br/>per cell" }
PAY@{ icon: "sd:bank", label: "Payment processors<br/>authorise, capture" }
FC@{ icon: "sd:warehouse", label: "Fulfilment centres<br/>pick, pack, ship" }
end
APP ==> CDN
APP ==> GW
WEB ==> GW
GW ==> RT
RT ==> CART
RT ==> CHK
CART --> CDB
CHK --> CDB
CHK --> PRICE
CHK ==>|"reserve"| INV
CDB -.->|"order events"| Q
Q ==> PAY
Q ==> FC
class APP,WEB client
class CDN,GW,RT edge
class CDB store
class PRICE cache
class Q queue
class PAY external
class FC externalKEY INSIGHTS:
- Two consistency regimes. The cart chooses availability and merges; order placement chooses correctness and uses atomic conditional writes. Mixing them is the classic mistake in both directions
- Cells by customer. Cart, checkout and orders are customer-scoped, so a customer id is a natural partition key for cells; a cell failure is a percentage, not an outage
- Shared services are few and shuffle-sharded. Prices and stock are scoped by product, not customer, so they can't live in customer cells; they're shared, read-heavy where possible, and isolated by shuffle sharding
- The page doesn't wait for the bank. Placement reserves stock and records the order; payment and fulfilment run behind a queue
Data Flow: Ishaan Places His Order#
%% title: Two presses of Place your order, one order sequenceDiagram autonumber participant A as Ishaan's laptop participant R as Cell router participant C as Checkout participant P as Price and promo participant I as Inventory participant D as Cell tables A->>R: Place order, purchase id p-7f3, cart version 41 R->>C: Cell 17, admitted at tier 1 C->>D: Seen p-7f3? D-->>C: No C->>P: Re-price 3 lines P-->>C: Total matches the page C->>I: Reserve 3 lines, key p-7f3 I-->>C: Reserved at FC SEA-4 and FC PDX-2 C->>D: Order o-88 and token p-7f3, one transaction Note over A,C: The response is lost in a slow network A->>R: Place order again, purchase id p-7f3 R->>C: Cell 17 C->>D: Seen p-7f3? D-->>C: Yes, order o-88 C-->>A: Order placed, o-88
MEANWHILE, IN PARALLEL:
- The transaction that wrote order
o-88also wrote an event to the cell's outbox; a relay puts it on the cell's order queue - The order pipeline authorises Ishaan's card, marks the order confirmed, and releases one shipment to each fulfilment centre, each step keyed by the order id
High-level design
Q1/3 Why can't the cart and order placement use the same consistency choice?
Answer & explanation
Answer: B
Availability-first for the cart, correctness-first for the order.
Q2/3 Why are price and inventory outside the customer cells?
Answer & explanation
Answer: B
Cells follow the grain of the data. Product-scoped state is shared and isolated another way (shuffle sharding).
Q3/3 The second press of "Place your order" reaches the server. What returns the first order?
Answer & explanation
Answer: B
One id per intent, written in the same transaction as the order.
Phase 4: Deep Dives (20 minutes)#
Deep Dive 1: The Cart That Never Says No (Week 5 Concepts: Conflict Resolution, Consistency Models; Week 1: Replication Trade-offs)#
"The Dynamo paper is the best-documented shopping cart in the industry, so I'll start from it, then say what I'd keep and what I'd change. The requirement hasn't moved: an add is never refused and never lost."
This builds on Week 5, Day 4 · Conflict Resolution, Week 5, Day 1 · Consistency Models and Week 1, Day 2 · Replication Trade-offs.
The phone case comes back
Ishaan's cart has three items: monitor, keyboard, case. His phone, on a bad connection, adds headphones; its write reaches only some replicas. His laptop, reading an older replica, removes the case. Both writes are accepted. Now there are two versions of the cart: {monitor, keyboard, case, headphones} and {monitor, keyboard}. If the cart is merged by taking the union of items, what does Ishaan see? If it's resolved by "last write wins", what's lost? How could a merge keep the add and the remove?
One strong answer
- Union: {monitor, keyboard, case, headphones}. The headphones are kept, but the case he removed comes back. That's the behaviour the Dynamo paper accepted: "an 'add to cart' operation is never lost. However, deleted items can resurface."
- Last write wins: whichever write has the later timestamp wins whole. If the laptop's remove is later, the headphones vanish: a lost add, which the requirement forbids.
- Keep both: store the cart as a set of add operations, each with a unique tag, and make a remove delete only the tags it saw. The phone's add has a tag the laptop never saw, so it survives; the laptop's remove names the case's tag, so the case stays removed. The merged cart is {monitor, keyboard, headphones}: what Ishaan meant.
What is published: the Dynamo paper (SOSP 2007) describes a store where "the result of each modification" is "a new and immutable version of the data", tracked with vector clocks, lists of (node, counter) pairs. When two versions have no causal order, "both versions of the data must be kept and presented to a client (upon a read) for semantic reconciliation"; the cart "can choose to 'merge' the conflicting versions and return a single unified shopping cart". Both add and remove are writes; if the latest version isn't reachable, "the item is added to (or removed from) the older version and the divergent versions are reconciled later". Data is placed by consistent hashing with virtual nodes and replicated to N nodes; the common configuration was (N, R, W) = (3, 2, 2). Reads and writes use a "sloppy quorum" of "the first N healthy nodes", and a replica meant for a failed node is held elsewhere with a hint and handed back later; Merkle trees find replicas that drifted. Profiled for 24 hours, 99.94% of requests to the cart service saw exactly one version, 0.00057% two, 0.00047% three and 0.00009% four; the growth in divergent versions came "not by failures but due to the increase in number of concurrent writers", "usually triggered by busy robots (automated client programs) and rarely by humans". Dynamo answered 99.9995% of requests without timing out, with no data loss "to date".
%% title: Two writes from two devices, and a merge that keeps both
flowchart TD
V1["Cart v1 · clock Sx 2<br/>monitor, keyboard, case"]:::service
PH["Phone adds headphones<br/>coordinated by Sx · Sx 3"]:::core
LT["Laptop removes case<br/>coordinated by Sz · Sx 2, Sz 1"]:::core
READ{"Read finds two versions<br/>clocks not ordered"}
MERGE["Merge by tags<br/>add tags kept, observed tags removed"]:::ok
V4["Merged cart · Sx 4, Sz 1<br/>monitor, keyboard, headphones"]:::ok
V1 ==> PH
V1 ==> LT
PH ==> READ
LT ==> READ
READ ==> MERGE ==> V4What we'd build today. Amazon hasn't published how today's cart is stored. Our design keeps Dynamo's promise and fixes its known wart:
- The cart is a set of lines, not a blob. Each add creates a line with a unique tag (device id plus a counter); quantity changes and removes name the tags they saw. This is an observed-remove set, a standard conflict-free replicated data type: merging two versions is a union of tags minus the removed tags, and it's the same whatever order the merges happen in.
- Removes leave tombstones. A removed tag is remembered (with a timestamp) long enough for any lagging replica or offline device to catch up, then garbage-collected. That's what stops the phone case resurfacing.
- Quantities merge by intent. "Set quantity to 2" on one device and "set quantity to 3" on another is a real conflict; take the higher quantity for the same tag and show a banner ("We updated the quantity of 1 item"). Losing a sale is worse than asking.
- Single-writer in the common case. The cart is keyed by customer id. On a store with strongly consistent, conditional writes on one key (as DynamoDB offers), the normal path is a conditional update on the cart's version: no divergence at all. The merge is the fallback for the rare cases: an offline device syncing, a region failover, a guest cart merging at sign-in.
- Never refuse the add. If the cart's partition can't be reached within its latency budget, the add goes to a local, durable add-log in the same cell (another table, another partition) and is merged into the cart on the next read. The customer sees "Added to Cart" either way.
Dynamo, 2007 (published)
- Leaderless; any of the top N nodes coordinates a write
- Sloppy quorum, hinted handoff: writes accepted during failures
- Divergent versions returned to the cart, merged by union
- An add is never lost; a removed item can come back
This design, today (ours)
- One leader per partition; conditional write on the cart version
- Add-log fallback in the same cell when the cart key is unreachable
- Lines with tags; removes name observed tags; tombstones
- An add is never lost; a remove stays removed
# cart/merge.py
"""
Merging divergent versions of a cart (illustrative). Each add creates a line with a
unique tag; a remove deletes only the tags it observed. Merging is a union of lines
minus the union of removed tags, so concurrent adds survive and removed items stay
removed. Quantities for the same tag merge to the larger value.
"""
from dataclasses import dataclass, field
@dataclass
class Cart:
lines: dict = field(default_factory=dict) # tag -> {"sku": str, "qty": int}
removed: dict = field(default_factory=dict) # tag -> removed_at (tombstones)
def add(cart: Cart, sku: str, qty: int, device: str, counter: int) -> str:
tag = f"{device}:{counter}" # unique per device, never reused
cart.lines[tag] = {"sku": sku, "qty": qty}
return tag
def remove_sku(cart: Cart, sku: str, now: float) -> None:
for tag, line in list(cart.lines.items()): # only the tags this device has seen
if line["sku"] == sku:
del cart.lines[tag]
cart.removed[tag] = now
def merge(a: Cart, b: Cart) -> Cart:
removed = {**a.removed, **b.removed}
lines = {}
for tag in (a.lines.keys() | b.lines.keys()) - removed.keys():
qa = a.lines.get(tag, {}).get("qty", 0)
qb = b.lines.get(tag, {}).get("qty", 0)
lines[tag] = {"sku": (a.lines.get(tag) or b.lines[tag])["sku"], "qty": max(qa, qb)}
return Cart(lines=lines, removed=removed)
def view(cart: Cart) -> dict:
out = {}
for line in cart.lines.values(): # two tags for one SKU: show one line
out[line["sku"]] = out.get(line["sku"], 0) + line["qty"]
return outDeep Dive 2: From Dynamo to DynamoDB: Partitions, Hot Keys and the Peak (Week 1 Concepts: Partitioning, Hot Keys; Week 10: Capacity Planning)#
"Dynamo solved availability but each team ran its own. DynamoDB made it a service and, along the way, changed most of the design. The parts that matter for Prime Day are how it splits tables, how it decides what to admit, and what it can't fix for you."
This builds on Week 1, Day 1 · Partitioning Deep Dive, Week 1, Day 4 · Hot Keys and Skew and Week 10, Day 4 · Capacity Planning.
A table that has never seen Prime Day
The orders table in one cell normally peaks at 300 writes a second. On Prime Day it will see 1,500. The deal-stock table has one item for the headphones deal that will take 5,000 writes a second in its first minute. A partition in the DynamoDB paper's example takes at most 1,000 write units a second; on-demand tables "instantly" accommodate up to double the previous peak; splitting a partition for traffic takes "minutes" and doesn't help a single hot item. Which of these two tables is a problem, and what do you do about each before the day?
One strong answer
- The orders table: 1,500 is 5× its previous peak, more than the instant 2×. Splits would catch up in minutes, but minutes of throttling at the opening is exactly what you can't afford. Drive the table to the expected peak in a load test days before, so its partitions are already split, then let traffic fall back. (Our practice, based on the paper's rule.)
- The deal item: no amount of splitting helps one item. Change the data model: split the deal's stock across ~20 items with different partition keys (Deep Dive 5), so each takes ~250 writes a second.
- Both: test past the expected peak, because the 2× rule is about the table's own history, and the day is not like any other day.
What is published: the DynamoDB paper (USENIX ATC 2022) says Dynamo "was created in response to the need for a highly scalable, available, and durable key-value database for shopping cart data", was "a single-tenant system", and that "teams were responsible for managing their own Dynamo installations", which "became a barrier to adoption". DynamoDB, launched in 2012, splits each table into partitions, each replicated across Availability Zones; a partition's replicas use Multi-Paxos, only the leader serves writes and strongly consistent reads, and a write is acknowledged "once a quorum of peers persists the log record". A healthy write quorum is "two out of the three replicas from different AZs". Every write can carry a condition, and multi-item ACID transactions are supported. Early on, throughput was divided evenly across a table's partitions, which caused "hot partitions" and "throughput dilution"; bursting let partitions use capacity unused in the last 300 seconds, and adaptive capacity "eliminated over 99.99% of the throttling due to skewed access pattern". It was replaced by global admission control, token buckets tracked centrally and handed to request routers, plus splitting for consumption, where a hot partition is split at a point chosen from its observed keys; splits "usually complete in the order of minutes", but a partition "receiving high traffic to a single item" won't benefit. On-demand tables accommodate "up to double the previous peak traffic on the table". For Prime Day 2021 the paper reports "trillions of API calls" from Amazon's systems over 66 hours, peaking at 89.2 million requests a second.
%% title: One write to DynamoDB, as the 2022 paper describes it
flowchart TD
APP["Cart or order service<br/>conditional write"]:::service
RR@{ icon: "sd:router", label: "Request router<br/>auth, local token bucket" }
GAC@{ icon: "sd:servers", label: "Global admission control<br/>vends tokens every few seconds" }
MD@{ icon: "sd:index", label: "Partition map<br/>MemDS, refreshed constantly" }
LEAD@{ icon: "sd:database", label: "Partition leader · AZ a<br/>writes, consistent reads" }
F2@{ icon: "sd:database", label: "Replica · AZ b" }
F3@{ icon: "sd:database", label: "Replica or log replica · AZ c" }
APP ==> RR
RR -.-> GAC
RR -.-> MD
RR ==> LEAD
LEAD ==>|"log record, quorum 2 of 3"| F2
LEAD --> F3
class RR edge
class GAC,MD service
class LEAD,F2,F3 store- Pick keys that spread. Carts by customer id, orders by order id (with a customer-to-orders index), idempotency records by customer plus purchase id. Every one of these is high-cardinality and evenly used; none is hot by design.
- Name the hot keys and design them away. The only keys everyone touches are a limited deal's stock and the counters behind "% claimed". Shard them (Deep Dive 5). A partition split can't help a single item, and a bigger table can't either.
- Pre-warm by rehearsal. The on-demand rule is relative to a table's own history. Each table's Prime Day peak is forecast per cell; anything above twice its previous peak is driven there in a load test before the day.
- Use conditions, not read-then-write. Cart version checks, idempotency records and stock decrements are all conditional writes: the store decides atomically, so no service needs a lock.
- Predictability over efficiency. The paper's lesson: "do not allow [caches] to hide the work that would be performed in their absence". If the price cache's hit rate drops on the day, the price service behind it must still be sized for the traffic it will see, or the cache's failure becomes the service's failure.
# capacity/prewarm.py
"""
Which tables need rehearsal before the peak (illustrative). An on-demand table
absorbs up to twice its previous peak at once; above that it splits partitions,
which takes minutes. Anything forecast above 2x its own history is load-tested to
its forecast first. A single hot item is flagged for write sharding instead.
"""
PARTITION_WRITES = 1000 # the DynamoDB paper's example per-partition maximum
def plan(tables: list) -> list:
actions = []
for t in tables: # {"name", "previous_peak_wps", "forecast_wps", "hottest_item_wps"}
if t["hottest_item_wps"] > 0.5 * PARTITION_WRITES:
shards = -(-t["hottest_item_wps"] * 2 // PARTITION_WRITES)
actions.append((t["name"], f"shard hottest item into >= {shards} keys"))
if t["forecast_wps"] > 2 * t["previous_peak_wps"]:
actions.append((t["name"], f"rehearse at {t['forecast_wps']} writes/s before the day"))
return actionsThe cart and the database
Q1/4 The Dynamo cart merged divergent versions by union. What could the customer see?
Answer & explanation
Answer: B
"An add to cart operation is never lost. However, deleted items can resurface."
Q2/4 How does a cart built from tagged lines keep both a concurrent add and a remove?
Answer & explanation
Answer: C
An observed-remove set: union of adds, minus observed removes, plus tombstones.
Q3/4 A single deal item takes 5,000 writes a second. Why won't DynamoDB's split for consumption fix it?
Answer & explanation
Answer: B
Split the item itself into many keys.
Q4/4 An orders table will see 5× its previous peak on Prime Day. What's the safe move?
Answer & explanation
Answer: B
On-demand absorbs up to 2× the table's previous peak at once; beyond that, splits take minutes.
Deep Dive 3: Cells and Shuffle Sharding: Making Every Failure Small (Week 9 Concepts: Tenant Isolation, Noisy Neighbours; Week 10: Deployment Strategies)#
"On a day like this, the scariest failure isn't a disk. It's us: a deploy with a bug, a config push, a poison request that crashes every server that touches it. Replication doesn't help with those, because every replica runs the same bug. Only isolation does."
This builds on Week 9, Day 1 · Tenant Isolation, Week 9, Day 2 · Noisy Neighbour and Week 10, Day 3 · Deployment Strategies.
100% at 5%, or 5% at 100%?
AWS's cell-based architecture guidance asks: "Is it better for 100% of customers to experience a 5% failure rate, or 5% of customers to experience a 100% failure rate?" Your checkout runs on one big fleet. A new build has a bug that crashes on one rare cart shape. What happens to the fleet when a customer with that cart retries? Now split customers into 40 cells of the full stack: what happens? And the price service, which every cell calls, is 32 groups of servers. If each cell is assigned 4 of the 32 groups and one cell sends a poison request, how many cells lose all their price capacity?
One strong answer
- One fleet: the poison cart crashes a server; the retry lands on another and crashes it too; the load balancer moves traffic to the survivors, which then crash. The Builders' Library describes such a problem cascading "through the other workers" until it takes out "all of the workers, and the entire service". Every customer is affected.
- 40 cells: the same request can only reach its own cell. At worst, 1/40 = 2.5% of customers lose checkout until the build is rolled back, and the build was only in one cell anyway, because deploys go cell by cell.
- Shuffle-sharded prices: with 4 of 32 groups per cell there are C(32, 4) = 35,960 combinations, so each of the 40 cells can have its own, sharing at most 2 groups with any other. A poison request from one cell takes down its 4 groups; every other cell still has at least 2 healthy groups. One cell is fully affected.
What is published: AWS's guidance Reducing the Scope of Impact with Cell-Based Architecture (September 2023) says that "for more than a decade, our service teams have used cell-based architecture". A cell is an independent instance of a workload that "does not share state with other cells"; a thin routing layer sends each request to a cell by a partition key such as a customer id, chosen to "match the grain of the service"; cells have a fixed maximum size and grow by adding cells, so each can be "stress tested and pushed past their breaking point"; and "if a workload uses 10 cells to service 100 requests, when a failure occurs in one cell, 90% of the overall requests would be unaffected". The Builders' Library article Workload isolation using shuffle-sharding (2019) explains the idea with 8 workers: plain sharding into 4 pairs limits an attack to 25%; assigning each customer a random pair of the 8 gives 28 possible "shuffle shards", so a problem reaches "just 1/28th". Route 53 uses 2,048 virtual name servers and gives each domain 4 of them: "730 billion possible shuffle shards", and no domain shares "more than two virtual name servers with any other customer domain".
%% title: Customers in cells; product-scoped services shuffle-sharded
flowchart TD
RT@{ icon: "sd:router", label: "Cell router<br/>map in memory, from a file" }
C1["Cell 1<br/>cart, checkout, orders, tables"]:::core
C17["Cell 17 · Ishaan<br/>cart, checkout, orders, tables"]:::hot
C40["Cell 40<br/>cart, checkout, orders, tables"]:::core
SS@{ icon: "sd:hexagons", label: "Price service · 32 groups<br/>each cell uses 4" }
INV@{ icon: "sd:database", label: "Inventory<br/>partitioned by product and FC" }
CP@{ icon: "sd:dashboard", label: "Control plane<br/>place, deploy, migrate cells" }
RT ==> C1
RT ==> C17
RT ==> C40
C17 ==> SS
C17 ==> INV
CP -.->|"one cell at a time"| C17
class RT edge
class SS service
class INV store
class CP serviceWhat we'd build (our design; Amazon hasn't published its retail cells):
- Partition key: customer id. Cart, checkout, orders and idempotency records are all scoped to one customer, the grain of the service. A cell holds a fixed maximum, say 8 million customers (assumed); ~300 million active carts (also assumed) → ~40 cells. At the design peak, each cell takes ~10,000 cart operations and ~500 orders a second.
- The router is dumb and static. It hashes the customer id to a cell from a mapping file loaded into memory. If the control plane is down, routing still works from the last mapping: the static stability the guidance asks for ("the data plane to continue operating even if the control plane is down").
- Deploys go cell by cell. A new build goes to one cell, bakes, then to a few, then to the rest, with automatic rollback on the cell's own alarms. A bad build is a 2.5% problem for minutes, not a 100% problem.
- Cells are tested to break. Because every cell is the same size, the load test for Prime Day is a test of one cell pushed past its forecast, not a guess about the whole fleet.
- Shared services get shuffle shards. Price and promotion lookups and the inventory service's request handlers are product-scoped, so they can't live in customer cells. Each cell is assigned its own combination of 4 of 32 server groups, so a poison request or a flood from one cell can't take all the capacity of any other cell.
# cells/shards.py
"""
Cell routing and shuffle-shard assignment (illustrative). Customers map to cells by
a hash over a static table loaded at start-up; each cell gets its own combination of
shared server groups, with no two cells sharing more than two groups.
"""
import hashlib
import itertools
import random
CELLS = 40
GROUPS = 32 # price service server groups
PER_CELL = 4
def cell_of(customer_id: str, mapping: list) -> int:
h = int(hashlib.sha256(customer_id.encode()).hexdigest(), 16)
return mapping[h % len(mapping)] # mapping: slots -> cell, from the control plane
def assign_shuffle_shards(seed: int = 7) -> dict:
rng = random.Random(seed)
combos = list(itertools.combinations(range(GROUPS), PER_CELL)) # 35,960 of them
rng.shuffle(combos)
chosen = {}
for combo in combos:
if all(len(set(combo) & set(other)) <= 2 for other in chosen.values()):
chosen[len(chosen)] = combo
if len(chosen) == CELLS:
break
return chosen # cell -> 4 groups, overlap <= 2 with any cellDeep Dive 4: Placing the Order Exactly Once (Week 2 Concepts: Idempotency, Timeout Hell; Week 3: Transactional Outbox)#
"The network gives us at-least-once: a request that timed out may or may not have happened. The customer wants exactly-once. The gap is closed by one thing the client sends and one write the server makes atomically."
This builds on Week 2, Day 2 · Idempotency in Practice, Week 2, Day 1 · Timeout Hell and Week 3, Day 2 · Transactional Outbox.
Nine seconds of spinner
Ishaan presses "Place your order". The request reaches checkout, which reserves stock and writes the order, but the response is lost. The app retries automatically after 3 seconds; then Ishaan presses again. Meanwhile he had a second tab open where he added a charger, and that tab's "Place your order" goes out too. Which of these requests should create an order? How does the server tell a retry from a new intent? What should the retry return: an error saying "already exists", or the order? And how long must the server remember?
One strong answer
- One intent, one id. The checkout page creates a purchase id when it renders. The automatic retry and the second press of the same page carry the same id: they're the same intent, and must return order
o-88. The second tab rendered its own checkout page, so it has its own id and a different cart: that's a second intent, and the customer gets a clear "you placed a similar order a minute ago, place this one too?" prompt rather than a silent duplicate. - Same id, different contents: if a request arrives with Ishaan's first purchase id but a different cart or total, it's not a retry. Reject it with a parameter-mismatch error instead of guessing.
- Return the order, not an error. A retry should get a semantically equivalent response, "Order placed, o-88", so the client can treat retries as invisible. "Already exists" pushes the uncertainty back to the caller.
- Remember with the order. The id lives in the same record as the order and can be looked up for as long as the order exists, plus a buffer for very late retries.
What is published: the Builders' Library article Making retries safe with idempotent APIs (Malcolm Featonby) describes Amazon's "preferred approach": "a unique caller-provided client request identifier" in the API contract (the ClientToken in EC2's RunInstances). On a first request, the service records the identifier and does the work; "the process that combines recording the idempotent token and all mutating operations related to servicing the request must meet the properties for an atomic, consistent, isolated, and durable (ACID) operation". A repeat returns "a semantically equivalent response", rather than a "ResourceAlreadyExists" code that leaves the caller unsure who created it. If the same token arrives with different parameters, "we return a validation error indicating a parameter mismatch", so the parameters are stored with the token. Retention for EC2 is "the lifetime of the resource, plus an interval" after which late retries are no longer plausible. Marc Brooker's Timeouts, retries, and backoff with jitter adds the other half: retries are "selfish"; a five-deep call stack with three tries at each layer multiplies load on the database "243x", so Amazon's practice is "to retry at a single point in the stack", with capped exponential backoff, jitter, and a token bucket limiting retries, which AWS added to its SDK in 2016. Timeouts are set from the dependency's latency at a chosen false-timeout rate, for example its p99.9 for 0.1%.
%% title: Place order with a purchase id, a retry, and a mismatched replay sequenceDiagram autonumber participant A as App participant C as Checkout participant I as Inventory participant D as Cell tables A->>C: Place order, id p-7f3, cart hash h1 C->>I: Reserve lines, key p-7f3 I-->>C: Reserved C->>D: Transaction · put token p-7f3 if absent · put order o-88 · put outbox row D-->>C: Committed Note over A,C: Response lost, app retries after 3 s with jitter A->>C: Place order, id p-7f3, cart hash h1 C->>D: Get token p-7f3 D-->>C: Order o-88, cart hash h1 C-->>A: Order placed, o-88 A->>C: Place order, id p-7f3, cart hash h2 C->>D: Get token p-7f3 D-->>C: Order o-88, cart hash h1 C-->>A: 409 parameter mismatch, review your cart
- The id comes from the page, not the click. Generating it at render time makes a double-press and an app retry the same intent. Generating it per click would turn a double-press into two orders.
- One transaction, three items. The token record (conditional: only if absent), the order, and an outbox row for the pipeline commit together. DynamoDB's ACID transactions across items in one table or several make this a single call. A crash leaves either all three or none.
- Reservation is keyed too. The inventory reservation carries the purchase id, so a retry that reached inventory twice still holds one set of units. If the order transaction fails, the reservation is released by the same key.
- A race between two retries. Two copies of the same request can both miss the token and both reserve; only one transaction's "put if absent" succeeds. The loser reads the winner's order, releases nothing (its reservation key is the same), and returns the winner's response.
- Retry once, at the edge. The app retries with jitter; checkout does not retry its own downstream calls more than once, and inventory does not retry at all. Three layers of retries are how one slow partition becomes 27× the load.
# checkout/place_order.py
"""
Placing an order exactly once (illustrative). The purchase id comes from the
rendered checkout page. The token, the order and an outbox row commit in one
transaction; a retry returns the same order, and a replay with different contents
is rejected as a mismatch.
"""
import hashlib
import json
class Mismatch(Exception):
pass
def cart_hash(lines: list, total_cents: int, address_id: str) -> str:
body = json.dumps({"l": sorted(lines), "t": total_cents, "a": address_id})
return hashlib.sha256(body.encode()).hexdigest()[:16]
def place_order(db, inventory, customer: str, purchase_id: str, lines: list,
total_cents: int, address_id: str) -> dict:
h = cart_hash(lines, total_cents, address_id)
key = f"{customer}#{purchase_id}"
seen = db.get("tokens", key)
if seen:
if seen["hash"] != h:
raise Mismatch(purchase_id) # same id, different intent
return {"order_id": seen["order_id"], "status": "PLACED"}
held = inventory.reserve(lines, key=key) # idempotent by key
order_id = f"o-{hashlib.sha256(key.encode()).hexdigest()[:10]}"
ok = db.transact([
("put_if_absent", "tokens", key, {"order_id": order_id, "hash": h}),
("put", "orders", order_id, {"customer": customer, "lines": lines,
"fcs": held["fcs"], "state": "PLACED"}),
("put", "outbox", order_id, {"event": "OrderPlaced"}),
])
if not ok: # another copy of this request won
seen = db.get("tokens", key)
if seen["hash"] != h:
inventory.release(key)
raise Mismatch(purchase_id)
return {"order_id": seen["order_id"], "status": "PLACED"}
return {"order_id": order_id, "status": "PLACED"}Deep Dive 5: Reserving Stock Without Overselling (Week 5 Concepts: Consistency Models; Week 1: Hot Keys and Skew)#
"The cart is allowed to be optimistic about stock; the order is not. So availability in the cart is a cached hint, and the reservation at placement is the only decision. The interesting parts are where stock lives and the few products everyone wants at once."
This builds on Week 5, Day 1 · Consistency Models and Week 1, Day 4 · Hot Keys and Skew.
20,000 headphones, 150,000 buyers
The headphones deal has 20,000 units: 12,000 at a fulfilment centre near Seattle and 8,000 near Dallas. 150,000 people try to buy in two minutes. When should a unit be taken: at "Add to Cart", at checkout, or at "Place your order"? Where does Ishaan's unit come from? One stock counter takes ~5,000 writes a second at the start: what's your data model? And the deal page shows "62% claimed": does that number have to be exact?
One strong answer
- At "Place your order". At add-to-cart, millions of carts would hold stock nobody buys; Amazon's cart is meant to accept adds freely. At placement, the reservation is the order's first step and either succeeds now or tells the customer now.
- The nearest centre that has it and can meet the promise. Ishaan's unit comes from Seattle. If Seattle's share is gone, Dallas, with a later delivery date shown before he confirms.
- Split the counter. Each centre's deal stock is split into sub-counters, say 12 for Seattle and 8 for Dallas, 1,000 units each, on different partition keys. A reservation picks a random sub-counter with stock and decrements it conditionally ("only if at least 1"). No sub-counter goes below zero, and the sub-counters sum to 20,000, so the deal can't oversell.
- "% claimed" is a display. It's summed from the sub-counters every second or two and cached. It can lag; the reservation can't.
What is published: Amazon has not published how it reserves stock or runs limited-quantity deals. What is published are the building blocks: DynamoDB's conditional writes and transactions, its note that a single hot item can't be split away (DynamoDB paper, 2022), and that DynamoDB serves "all Amazon fulfillment centers" (AWS, Prime Day posts). The design in this deep dive is ours. Flipkart's sale (Bonus Problem 27) adds a waiting line in front of a deal; here we show the counter design underneath.
%% title: Units of stock for one deal at one fulfilment centre stateDiagram-v2 [*] --> AVAILABLE: received at the FC AVAILABLE --> RESERVED: order placed, key p-7f3 RESERVED --> AVAILABLE: order failed or cancelled, released by key RESERVED --> ALLOCATED: payment authorised ALLOCATED --> PICKED: released to the warehouse PICKED --> SHIPPED: left the building ALLOCATED --> AVAILABLE: order cancelled before pick SHIPPED --> [*] class SHIPPED ok class RESERVED queue
- One row per product per fulfilment centre, with
on_hand,reservedandallocated; available = on_hand − reserved − allocated. Every move is a conditional update on that row; a reconciler checks the arithmetic against warehouse counts. - Reservations are records, keyed by the purchase id, with the lines and centres they hold. A retry finds the record; a release names it. No reservation is anonymous.
- No clock-based expiry for placed orders. A reservation belongs to an order, and the order's state machine releases it (Deep Dive 6). A sweeper only cleans reservations whose order transaction never committed (the crash between reserve and commit), after a few minutes.
- Sub-counters only where needed. Ordinary products have one row per centre and a few writes a second. Only products forecast to be hot (deals, launches) are split, before the deal goes live; when few units remain, a sweeper gathers the stragglers into one sub-counter so the last units still sell.
- Multi-line orders reserve together. The monitor, keyboard and headphones are reserved in one transaction across their rows (DynamoDB transactions cover multiple items atomically). If the headphones are gone, nothing is held, and the page says which item is sold out.
# inventory/reserve.py
"""
Reserving deal stock from split counters (illustrative). Each sub-counter can only
be decremented while it has stock, and the sub-counters sum to the deal quantity,
so the deal cannot oversell. The reservation is a record keyed by the purchase id.
"""
import random
TRIES = 3
class SoldOut(Exception):
pass
def reserve_deal_unit(db, deal: str, fcs: list, key: str) -> dict:
existing = db.get("reservations", key)
if existing:
return existing # a retry: same units
for fc in fcs: # nearest centre first
subs = db.query("deal_stock", prefix=f"{deal}#{fc}#")
live = [s["id"] for s in subs if s["left"] > 0]
for sub in random.sample(live, min(TRIES, len(live))):
ok = db.transact([
("update_if", "deal_stock", sub, {"left": -1}, "left >= 1"),
("put_if_absent", "reservations", key,
{"deal": deal, "fc": fc, "sub": sub, "state": "RESERVED"}),
])
if ok:
return db.get("reservations", key)
raise SoldOut(deal)
def release(db, key: str) -> None:
r = db.get("reservations", key)
if r and r["state"] == "RESERVED":
db.transact([
("update_if", "reservations", key, {"state": "RELEASED"}, "state = RESERVED"),
("update", "deal_stock", r["sub"], {"left": +1}),
])Deep Dive 6: The Order Pipeline After "Place Your Order" (Week 3 Concepts: Queues, Backpressure; Week 5: Saga Orchestration)#
"Once the order is placed, the customer is gone. Everything after that is asynchronous: payment, confirmation, the hand-off to fulfilment. Queues make that durable, and the Builders' Library is blunt about what queues do to you when they back up."
This builds on Week 3, Day 1 · Queue vs Stream, Week 3, Day 3 · Backpressure and Flow Control, Week 5, Day 3 · Saga Orchestration and Week 3, Day 4 · Dead Letters and Poison Pills.
A one-hour payment outage
At 10:00 on the first day, one payment processor stops answering. Orders keep being placed at 1,500 a second in Ishaan's region (assumed); 40% of them use that processor. It recovers at 11:00. How big is the backlog? If that processor's worker pool, and the processor itself, normally run at 50% of capacity, how long until the backlog is gone? What do new customers see while it drains? And should the pipeline work through the backlog in order?
One strong answer
- Backlog: 1,500 × 40% × 3,600 s ≈ 2.2 million orders waiting for authorisation.
- Drain time: that processor's new orders still arrive at the normal rate, using half its capacity; the other half drains the backlog at the same rate. Clearing an hour of backlog takes another hour at double the normal rate, which is the Builders' Library's arithmetic: "recovering from the outage requires double the system's capacity for another hour after the recovery". More workers help only if the processor itself can take the extra rate.
- Customers: placement doesn't wait for payment, so new customers still see "Order placed". Their confirmation email and delivery date may lag, and the dashboard should say so.
- Not strictly in order. Fresh orders go to the live queue and are authorised immediately; the backlog drains from a separate sideline queue as capacity allows. Otherwise every new customer waits behind an hour of old work.
What is published: the Builders' Library article Avoiding insurmountable queue backlogs uses "fulfilling orders placed on amazon.com" as an example of a workflow built on queues, and warns that "queueing that is meant to improve availability can backfire" by "dramatically" increasing recovery time. Its practices: measure the age of messages, not just the count; separate workloads into separate queues, or shuffle-shard customers across a fixed set of queues as Lambda does; prefer "LIFO-ish behavior" for fresh work and "sideline old traffic" into a backlog queue; and protect each layer, since "there will always be some workload that gets around the front-door admission control". Using load shedding to avoid overload adds that prioritisation should happen "as early as possible (such as in the service that receives the initial request from the client)", that a request for a search crawler is "likely to be less critical to serve than a request that originates from a human", and that servers should drop work whose client has already timed out. Amazon has not published its order pipeline's design; what follows is ours.
%% title: One order, from placed to released, every step keyed by the order id
flowchart TD
OB@{ icon: "sd:log", label: "Outbox row<br/>written with the order" }
LIVE@{ icon: "sd:queue", label: "Live queue · cell 17<br/>fresh orders first" }
SIDE@{ icon: "sd:queue", label: "Sideline queue<br/>old orders after a backlog" }
ORCH["Order orchestrator<br/>state machine per order"]:::hot
PAY@{ icon: "sd:card", label: "Payment authorisation<br/>key o-88, timeout from p99.9" }
REL["Release to fulfilment<br/>one shipment per FC"]:::core
COMP["Compensate<br/>release stock, notify"]:::bad
OB ==> LIVE
LIVE ==> ORCH
SIDE -.-> ORCH
ORCH ==> PAY
PAY ==>|"authorised"| REL
PAY -.->|"declined for good"| COMP
class OB store
class LIVE,SIDE queue
class PAY external- An orchestrated saga, not a chain of events. One orchestrator per order holds its state (
PLACED → AUTHORISED → CONFIRMED → RELEASED, orPAYMENT_PENDING_CUSTOMER → CANCELLED) and calls each step with the order id as the idempotency key. A crash resumes from the stored state; a repeated step is a no-op. - Authorise now, charge later. Placement records the order; the pipeline authorises the card (a hold), and the charge is captured when the shipment leaves the fulfilment centre. That's the card networks' authorise-then-capture model (Bonus Problem 4), and it means a cancelled-before-shipping order costs a released hold, not a refund. This is our design choice; Amazon's own charge timing isn't covered by the sources here.
- A timeout is "unknown", not "failed". If the processor doesn't answer within its timeout, the step is retried later with the same key, and never as a new authorisation. A decline after retries moves the order to
PAYMENT_PENDING_CUSTOMERand emails the customer; stock stays reserved for a fixed number of days, then the order is cancelled and released. - Queues per cell, ages on the dashboard. Each cell has its own live queue, so one cell's backlog doesn't delay another's orders. The alarm is the age of the oldest message in the live queue, not the queue length.
- Bulkheads per processor. Each payment processor gets its own worker pool and concurrency limit. A slow processor fills its own pool and its orders wait; orders for other processors flow.
- Release once. The hand-off to a fulfilment centre is a write keyed by (order id, centre). The centre's system ignores a second copy. A shipment can be cancelled until it's picked; after that, the customer's cancellation becomes a return.
# orders/orchestrator.py
"""
Advancing one order (illustrative). Each step is called with the order id as its
idempotency key, so a crash or redelivery repeats nothing. A processor timeout is
"unknown" and retried later with the same key; a final decline waits for the
customer, then cancels and releases stock.
"""
import random
PENDING_CUSTOMER_DAYS = 3
def advance(db, payments, fulfilment, inventory, order_id: str, now: float) -> str:
o = db.get("orders", order_id)
if o["state"] == "PLACED":
r = payments.authorise(order_id, o["payment_method"], o["total_cents"], key=order_id)
if r == "AUTHORISED":
return db.cas("orders", order_id, "PLACED", "AUTHORISED")
if r == "UNKNOWN":
return db.schedule(order_id, at=now + 30 + random.uniform(0, 30)) # backoff, jitter
db.cas("orders", order_id, "PLACED", "PAYMENT_PENDING_CUSTOMER",
deadline=now + PENDING_CUSTOMER_DAYS * 86400)
return "EMAIL_CUSTOMER"
if o["state"] == "AUTHORISED":
for fc in o["fcs"]:
fulfilment.release(order_id, fc, key=f"{order_id}#{fc}") # once per FC
return db.cas("orders", order_id, "AUTHORISED", "RELEASED")
if o["state"] == "PAYMENT_PENDING_CUSTOMER" and now > o["deadline"]:
inventory.release(o["reservation_key"])
return db.cas("orders", order_id, "PAYMENT_PENDING_CUSTOMER", "CANCELLED")
return o["state"]Cells, orders, stock and the pipeline
Q1/6 A new checkout build crashes on one rare cart shape. With 40 customer cells and cell-by-cell deploys, what's the worst case?
Answer & explanation
Answer: B
Replicas run the same bug. Isolation, not replication, bounds a bad deploy.
Q2/6 Why give each cell 4 of 32 price-service groups instead of 4 fixed shards of 8?
Answer & explanation
Answer: B
With fixed shards, a bad cell takes down its whole shard and every cell on it.
Q3/6 Ishaan's app retries "Place your order" with the same purchase id but his cart changed. What should the server do?
Answer & explanation
Answer: C
Same token, different parameters: not a retry. Amazon's guidance is a validation error.
Q4/6 Three layers each make 3 tries. How much load can one failing database see?
Answer & explanation
Answer: C
Retries multiply per layer; retry at a single point in the stack.
Q5/6 A deal's stock is split into 20 sub-counters. Why can't it oversell?
Answer & explanation
Answer: B
No shared "last unit" exists for two buyers to take.
Q6/6 A payment processor was down for an hour. Its worker pool and the processor normally run at 50% of capacity. Roughly how long until its backlog is gone?
Answer & explanation
Answer: B
Half the capacity serves new orders; the other half drains an hour of backlog in an hour.
Phase 5: Scaling and Edge Cases (5 minutes)#
"Walk me through Prime Day from the platform's side, then the cases that bite."
Ishaan's Prime Day, From the Platform's Side#
- Weeks before · Rehearse
- Every cell is load-tested past its forecast; tables forecast above 2× their history are driven to their peak; AWS reports more than 44,000 fault-injection experiments run to help keep Amazon.com available on Prime Day 2026
- Days before · Freeze and pre-split
- Deploys slow to a careful cadence; deal products get their stock split into sub-counters; price-service shuffle shards are checked for overlap
- 23 June, opening · The first peak
- Cart operations climb toward 100,000 a second; the front door sheds crawler traffic first; carts never refuse an add
- 8:10 am · The bus
- Ishaan's add reaches the cart through a weak signal; the cart key's write succeeds on the second try with the same tag
- 8:40 am · Two presses
- One purchase id, one reservation, one order; the retry returns
o-88
- One purchase id, one reservation, one order; the retry returns
- 8:41 am · Behind the page
- The orchestrator authorises the card, releases one shipment to Seattle and one to Portland
- 23–26 June · Four days
- Peaks repeat with each time zone's morning and each deal drop; cells and queues are watched by age and error rate, cell by cell
- After · Reconcile
- Every reservation is matched to an order or released; every authorisation is captured, released or expired
Edge Cases#
Edge case 1: A guest cart meets an account cart
Problem: Ishaan browsed signed out on his laptop and added a cable, then signed in, where his account cart has three items.
Solution: Merge the guest cart's lines into the account cart as new tags (Deep Dive 1); never replace one cart with the other. Show "1 item from your earlier session was added".
Edge case 2: Price changes between cart and order
Problem: The headphones deal ends while Ishaan is on the checkout page; the price rises.
Solution: Checkout re-prices at placement and compares with the total the page showed (part of the purchase-id hash). If it differs, reject with "prices changed, review your order" rather than charging a total he didn't see.
Edge case 3: Bots hammer one cart or one deal
Problem: Automated clients write the same cart concurrently, or flood a deal's reservations. The Dynamo paper found most divergent cart versions came from "busy robots".
Solution: Per-customer rate limits at the front door; a per-customer cap on deal units; bot traffic shed before human traffic under overload.
Edge case 4: A whole Availability Zone is lost
Problem: One of three zones goes dark at the peak.
Solution: Every cell runs in three zones with enough capacity in two to carry the load without launching anything (static stability). DynamoDB keeps a write quorum of two replicas in different zones; partitions whose leader was in the lost zone elect a new one after the old lease expires, which the paper says "only takes a couple of seconds".
Phase 6: Monitoring and Operations#
"Per cell, four questions: are cart writes being accepted, are orders placed once, is stock exact, and is the pipeline fresh? Every metric is per cell first, because a fleet-wide average hides a cell that's on fire."
Monitoring Dashboard#
Cart and checkout (cell 17 / all 40 cells)
| Metric | Cell 17 | All cells |
|---|---|---|
| Cart operations | 2,610/s | 98,400/s |
| Cart add p99.9 | 41 ms | 63 ms (worst cell) |
| Adds accepted via add-log fallback | 0.002% | 0.004% |
| Orders placed | 128/s | 4,870/s |
| Place order p99.9 | 640 ms | 1.1 s (worst cell) |
Exactly once
- Retries answered from the token table: 3.1% of placements; parameter mismatches: 0.02%; duplicate orders: 0
- Deal "headphones-d42": 6,210 units left across 20 sub-counters; oversold: 0
Pipeline
- Age of the oldest message, live queue (worst cell): 4 s; sideline queue: empty
- Payment authorisations unknown and awaiting retry: 1,940; processor B pool at 71%
SLOs for the Sale#
SLO 1: The cart never says no
- Target: 99.999% of cart adds acknowledged; p99.9 < 300 ms in every cell
SLO 2: Orders happen once
- Target: zero duplicate orders or authorisations from retries; place-order p99.9 < 2 s in every cell
SLO 3: Stock is exact
- Target: zero oversold deals; reservations reconcile to orders or releases daily
SLO 4: The pipeline is fresh
- Target: 99% of orders authorised and released within 5 minutes; oldest live message < 60 s
Reading the dashboard
Q1/2 The fleet-wide cart p99.9 looks fine, but cell 23's is 900 ms. What do you do?
Answer & explanation
Answer: B
Cells are measured as cells. An average across 40 hides a cell in trouble.
Q2/2 The live queue's oldest message is 20 minutes old, but the queue length looks normal. Likeliest cause?
Answer & explanation
Answer: B
Measure age, not just count.
Final Challenge: Prime Day 2027, With a Zone Down#
The interviewer leans back. "One more."
"It's the first morning of next year's Prime Day. Your forecast assumes the platform grows as it did from 2025 to 2026. At the peak hour, one Availability Zone in your busiest region goes dark. At the same moment, a new checkout build is live in 2 of your 40 cells, and a celebrity-promoted deal for 50,000 speakers goes live. Tell me what happens to carts, orders, stock and payments."
- DynamoDB peaked at 151 million requests a second in 2025 and 192 million in 2026 (published): about +27%; the same growth gives ~244 million in 2027 (assumption)
- Our design peak: ~6,400 orders and ~127,000 cart operations a second (2026 estimates × 1.27, assumed)
- 1 of 3 zones lost in the busiest region at the peak hour
- New checkout build in 2 of 40 cells
- Deal: 50,000 units across 4 fulfilment centres; 400,000 buyers in 3 minutes (assumed)
What do you change, and what holds on its own?
Decide what each failure does to the design without anyone touching it, what your on-call does in the first ten minutes, and what you'd have changed before the day.
One strong answer
- Zone loss holds on its own. Each cell was provisioned to carry its peak in two zones (static stability), so nothing needs launching. DynamoDB partitions keep a two-zone write quorum; those whose leader was in the lost zone elect a new one in seconds. The cell router reads its mapping from memory, so routing doesn't depend on the control plane. Some in-flight requests fail and are retried by the app with the same tags and purchase ids, so no add is lost and no order is duplicated.
- The new build stays small. It's in 2 of 40 cells, so at most 5% of customers see it. On-call's first move with any alarm in those cells during a zone event is to roll them back, not to debug; a deploy freeze should have stopped it in the first place.
- The deal is a counter problem, not a zone problem. 50,000 units in 4 centres, split into 50 sub-counters of 1,000 spread across partitions; 400,000 buyers in 3 minutes is ~2,200 attempts a second, ~45 per sub-counter. If one centre's sub-counters were in the lost zone's leaders, they're back within seconds; reservations go to the next centre meanwhile.
- Payments drain, not fail. Authorisations that timed out are unknown and retried with the same key; fresh orders go to the live queue, any backlog to the sideline queue.
- What you'd measure: cart adds acknowledged (99.999%), duplicate orders (zero), oversold units (zero), oldest live message, and errors per cell, with the 2 new-build cells compared against the other 38.
Final challenge · Quest check
Q1/3 A zone is lost at peak. Why doesn't the system need to launch capacity?
Answer & explanation
Answer: B
Prepare for the impairment before it happens.
Q2/3 Two cells run a new build during the zone loss. What's the first move if those cells alarm?
Answer & explanation
Answer: B
Cells and cell-by-cell deploys keep a bad build small; rollback is the first response.
Q3/3 A retried "Place your order" during the zone loss: what prevents a second order?
Answer & explanation
Answer: B
The idempotency token is decided by the store, whatever failed in between.
Interview Conclusion#
"Why not make the cart strongly consistent and simply return an error when its partition is unavailable? It's simpler."
"Because an error on 'Add to Cart' is a lost sale on the busiest day of the year, and the Dynamo paper's whole point was that the cart must not refuse writes. Today I'd use strongly consistent conditional writes on the normal path, since a well-run partitioned store gives us that almost always, and keep a merge path with tagged lines and tombstones for the rare cases. That keeps adds, and unlike the 2007 union, it keeps removes too."
"And why not make order placement eventually consistent, like the cart?"
"Because nothing merges two orders back into one after a customer has been charged twice. The order is the one place we pay for an atomic, conditional write: the purchase id, the order and the outbox row in one transaction. Everything else, payment, confirmation, fulfilment, runs behind it asynchronously."
"The single most important decision?"
"Cells. Prime Day's worst risk isn't load we didn't forecast, it's a mistake we made: a deploy, a config, a poison request. Replication copies mistakes to every replica. Cells, cell-by-cell deploys and shuffle-sharded shared services turn 'everyone is down' into 'one in forty for a few minutes'."
Summary: Concepts Applied from 10-Week Course#
- Week 1: Data at scale
- Partitioning — carts by customer, orders by order id, stock by product and fulfilment centre
- Hot keys — a deal's stock split into sub-counters; per-cart rate limits
- Replication trade-offs — Dynamo's (3, 2, 2) quorums and sloppy quorum; DynamoDB's leader and two-zone write quorum
- Week 2: Failure-first design
- Idempotency — purchase ids recorded with the order; reservation and payment keys
- Timeouts and retries — retry at one layer, backoff with jitter, timeouts from p99.9
- Week 3: Messaging and async processing
- Transactional outbox — order and pipeline event in one transaction
- Backpressure — live and sideline queues, age of oldest message, shedding crawlers first
- Week 5: Consistency and coordination
- Conflict resolution — vector clocks and an observed-remove cart
- Saga orchestration — reserve, authorise, release, compensate
- Week 9: Multi-tenancy
- Tenant isolation and noisy neighbours — customer cells, shuffle-sharded shared services
- Week 10: Production readiness
- Capacity planning — rehearse tables above 2× their history; static stability across zones
- Deployment strategies — one cell at a time
Why Amazon Matters#
The cart wrote the book
- The Dynamo paper (2007) turned one product requirement, "never refuse an add to cart", into a database design that shaped a generation of distributed stores. Its successor, DynamoDB, now carries Amazon's own peak days at 192 million requests a second
Two consistency choices in one flow
- The cart is the textbook case for availability and merging; the order is the textbook case for atomic, idempotent writes. A good design uses each where it belongs and never confuses them
The real enemy is our own change
- On the busiest days, Amazon's published practice is isolation and rehearsal: cells, shuffle sharding, static stability, and tens of thousands of fault-injection experiments for the event
"Never refuse an add, never repeat an order, and never let one mistake reach every customer."
Self-Assessment Checklist#
After studying this case study, you should be able to:
The cart:
- Explain why the Dynamo cart was "always writeable" and why deleted items could resurface
- Design a cart merge that keeps concurrent adds and removes, with tags and tombstones
- Describe how DynamoDB's partitions, admission control and splits differ from Dynamo, and what they can't fix
Placing the order:
- Make order placement idempotent with a client-supplied id recorded atomically with the order
- Return semantically equivalent responses to retries and reject parameter mismatches
- Reserve stock at placement without overselling, including a hot deal
Surviving the day:
- Split customer-scoped services into cells and shuffle-shard shared ones
- Build an order pipeline whose backlogs drain fresh work first
- Plan capacity so a zone loss needs no new capacity, and rehearse tables above their history
Sources#
Papers:
- Paper
Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall and Werner Vogels — Dynamo: Amazon's Highly Available Key-value Store (cart "always writeable"; disks failing, routes flapping, "tornados"; "Add to Cart" never forgotten or rejected; merge and resurfacing deletes; vector clocks; (3,2,2); sloppy quorum and hinted handoff; Merkle trees; 150+ services per page; 99.9th-percentile SLAs and the 300 ms example; tens of millions of requests and well over 3 million checkouts in a day; 99.94% one version over 24 hours; divergence from concurrent writers, "busy robots"; 99.9995% success), SOSP, October 2007: https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf
- Paper
Mostafa Elhemali, Niall Gallagher, Nicholas Gordon, Joseph Idziorek, Richard Krog, Colin Lazier, Erben Mo, Akhilesh Mritunjai, Somu Perianayagam, Tim Rath, Swami Sivasubramanian, James Christopher Sorenson III, Sroaj Sosothikul, Doug Terry and Akshat Vig — Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service (Dynamo created for shopping-cart data, single-tenant; DynamoDB 2012; partitions, Multi-Paxos, two-of-three AZ write quorum, log replicas; conditional writes and ACID transactions; 1,000-WCU partition example; bursting, adaptive capacity eliminating over 99.99% of skew throttling, global admission control, split for consumption in minutes, not for a single hot item; on-demand up to 2× the previous peak; lease takeover in a couple of seconds; Prime Day 2021: 66 hours, peak 89.2 million requests a second; "do not allow [caches] to hide the work"), USENIX ATC, July 2022: https://www.usenix.org/system/files/atc22-elhemali.pdf
AWS News Blog, Prime Day posts:
- Book
Jeff Barr — Prime Day 2017 – Powered by AWS (30-hour event; DynamoDB 3.34 trillion requests, peak 12.9 million a second), 14 September 2017: https://aws.amazon.com/blogs/aws/prime-day-2017-powered-by-aws/
- Book
Jeff Barr — Amazon Prime Day 2019 – Powered by AWS (48 hours; 7.11 trillion DynamoDB calls, peak 45.4 million a second; 442 fulfilment centres), 16 August 2019: https://aws.amazon.com/blogs/aws/amazon-prime-day-2019-powered-by-aws/
- Blog
Jeff Barr — Amazon Prime Day 2022 – AWS for the Win! (DynamoDB peak 105.2 million a second; SQS 70.5 million messages a second; more than 100,000 items a minute), 25 July 2022: https://aws.amazon.com/blogs/aws/amazon-prime-day-2022-aws-for-the-win/
- Book
Jeff Barr — Prime Day 2023 Powered by AWS – All the Numbers (DynamoDB peak 126 million a second; SQS 86 million messages a second), 2 August 2023: https://aws.amazon.com/blogs/aws/prime-day-2023-powered-by-aws-all-the-numbers/
- Blog
Channy Yun and Jeff Barr — How AWS powered Prime Day 2024 for record-breaking sales (two-day event; DynamoDB peak 146 million a second; 733 fault-injection experiments), 13 August 2024: https://aws.amazon.com/blogs/aws/how-aws-powered-prime-day-2024-for-record-breaking-sales/
- Blog
Channy Yun — AWS services scale to new heights for Prime Day 2025: key metrics and milestones (four-day event; DynamoDB peak 151 million a second; SQS 166 million messages a second; 6,800+ fault-injection experiments), 26 August 2025: https://aws.amazon.com/blogs/aws/aws-services-scale-to-new-heights-for-prime-day-2025-key-metrics-and-milestones/
- Book
Channy Yun — All the numbers: Amazon Prime Day 2026 powered by AWS (23–26 June 2026; DynamoDB 59 trillion+ requests, peak 192 million a second; SQS peak 213 million messages a second; 44,000+ fault-injection experiments, more than six times 2025), 6 October 2026: https://aws.amazon.com/blogs/aws/all-the-numbers-amazon-prime-day-2026-powered-by-aws/
Amazon Builders' Library and AWS guidance:
- Docs
Malcolm Featonby — Making retries safe with idempotent APIs (client request identifier; ACID recording of token and mutations; semantically equivalent responses; parameter-mismatch validation error; retention for the resource's lifetime plus an interval): https://aws.amazon.com/builders-library/making-retries-safe-with-idempotent-APIs/
- Read
Marc Brooker — Timeouts, retries, and backoff with jitter (timeouts from p99.9 at 0.1% false timeouts; retries are "selfish"; 243× at five layers; retry at a single point; token-bucket retry limits in the SDK since 2016; jitter), 2019: https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
- Read
Workload isolation using shuffle-sharding (8 workers, 28 shuffle shards, 1/28th impact; Route 53: 2,048 virtual name servers, 4 per domain, 730 billion shuffle shards, no more than two shared), 2019: https://aws.amazon.com/builders-library/workload-isolation-using-shuffle-sharding/
- Read
Avoiding insurmountable queue backlogs (order fulfilment as a queue workflow; double capacity for another hour after an hour's outage; age of messages; separate and shuffle-sharded queues; LIFO-ish behaviour; sidelining old traffic), 2019: https://aws.amazon.com/builders-library/avoiding-insurmountable-queue-backlogs/
- Read
Using load shedding to avoid overload (prioritise early, at the service receiving the request; crawler traffic below human traffic; drop work whose client timed out; bounded work), 2019: https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/
- Read
Static stability using Availability Zones (overprovision so the system keeps working without launching instances if a zone is impaired), 2019: https://aws.amazon.com/builders-library/static-stability-using-availability-zones/
- Book
AWS Well-Architected — Reducing the Scope of Impact with Cell-Based Architecture (cells used by AWS service teams for more than a decade; no shared state; partition key matching the grain of the service; fixed maximum cell size; 10 cells, 90% unaffected; control plane and data plane; cell migration; "100% of customers ... 5% failure rate, or 5% of customers ... 100%"), 20 September 2023: https://docs.aws.amazon.com/wellarchitected/latest/reducing-scope-of-impact-with-cell-based-architecture/reducing-scope-of-impact-with-cell-based-architecture.html
Amazon executives and engineers:
- Read
Werner Vogels — Amazon DynamoDB – a Fast and Scalable NoSQL Database Service Designed for Internet Scale Applications (outages at the height of the 2004 holiday season; Dynamo built for the shopping cart and session service), All Things Distributed, January 2012: https://www.allthingsdistributed.com/2012/01/amazon-dynamodb.html
- Read
Werner Vogels — A Decade of Dynamo (2004 Oracle limits; ~70% key-value and ~20% single-table operations; DynamoDB launched January 2012; Prime Day 2017 peak 12.9 million a second), All Things Distributed, 2 October 2017: https://www.allthingsdistributed.com/2017/10/a-decade-of-dynamo.html
- Blog
Jeff Barr — Migration Complete – Amazon's Consumer Business Just Turned off its Final Oracle Database (75 PB in ~7,500 Oracle databases; teams including Ordering, Consumer Payments and Retail Systems; low-latency services to DynamoDB, transactional relational workloads to Aurora and RDS), AWS News Blog, 15 October 2019: https://aws.amazon.com/blogs/aws/migration-complete-amazons-consumer-business-just-turned-off-its-final-oracle-database/
Design note: the cart's tagged-line merge, the add-log fallback, the cells and their size and count, the shuffle-shard layout, the purchase id and its hash, the reservation and sub-counter design, the orchestrated order pipeline, authorise-then-capture, the timeouts, thresholds, dashboards and every number labelled as an assumption or estimate are an illustrative design for the problem, not a description of Amazon's internal systems beyond what Amazon and AWS have published. Order rates, cart operations per order, growth factors, deal sizes, buyer counts and the 2027 projection are assumptions. Ishaan Mehta and everything he sees are illustrative.
Further Reading#
Related systems to study:
- Side quest
Flipkart Big Billion Days (Bonus Problem 27 in this series): a sale's opening minute, a waiting line for flash deals, stock holds and a payment surge through Indian banks
(opens in new tab) - Side quest
AWS S3 (Bonus Problem 8): another Amazon system built for durability at scale
(opens in new tab) - Side quest
Visa and Mastercard (Bonus Problem 4): authorise now, capture and settle later
(opens in new tab) - Side quest
BookMyShow (Bonus Problem 16): seats that must never be sold twice, under a flash crowd
(opens in new tab)
From the war stories:
- Read
- Read
AWS's hidden internal network jammed, and took its own monitoring down with it
- Read
Flipkart's founders said sorry the day after the Big Billion Day
Foundational concepts (this course):
- Lesson
Week 1, Day 1 — Partitioning Deep Dive
(opens in new tab) - Lesson
Week 1, Day 2 — Replication Trade-offs
(opens in new tab) - Lesson
Week 1, Day 4 — Hot Keys and Skew
(opens in new tab) - Lesson
Week 2, Day 1 — Timeout Hell
(opens in new tab) - Lesson
Week 2, Day 2 — Idempotency in Practice
(opens in new tab) - Lesson
Week 3, Day 1 — Queue vs Stream
(opens in new tab) - Lesson
Week 3, Day 2 — Transactional Outbox
(opens in new tab) - Lesson
Week 3, Day 3 — Backpressure and Flow Control
(opens in new tab) - Lesson
Week 3, Day 4 — Dead Letters and Poison Pills
(opens in new tab) - Lesson
Week 5, Day 1 — Consistency Models
(opens in new tab) - Lesson
Week 5, Day 3 — Saga Orchestration
(opens in new tab) - Lesson
Week 5, Day 4 — Conflict Resolution
(opens in new tab) - Lesson
Week 9, Day 1 — Tenant Isolation
(opens in new tab) - Lesson
Week 9, Day 2 — Noisy Neighbour
(opens in new tab) - Lesson
Week 10, Day 3 — Deployment Strategies
(opens in new tab) - Lesson
Week 10, Day 4 — Capacity Planning
(opens in new tab)
"Never refuse an add, never repeat an order, and never let one mistake reach every customer."
this lesson in shorts · 4
play them all →One idea from this lesson in about a minute each, made for your phone.
- 0/7stages cleared
Follow-up drills
0 of 11 practisedYou presented Amazon Prime Day Checkout. Now the interviewer pushes back. Answer each follow-up out loud or in the box, then reveal a strong answer.
“DynamoDB peaked at 192 million requests a second on Prime Day 2026. Is that the load you're designing the cart for?”
Pushes on · sizing a service from a platform total
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.



