The system design question
Design the checkout for an online store like Amazon. A customer reviews their cart, pays, and gets an order confirmation. We must never charge anyone twice, and we can't sell more of an item than we have.
Agree on the scope (5 minutes)
Say up front what you're covering.
| We'll design | We'll skip |
|---|---|
| Turning a cart into an order | Search and the catalog |
| Holding stock while someone pays | Shipping and delivery |
| Taking the payment | Returns and refunds |
| Confirming the order | Recommendations |
Then agree on what "good" means:
- Correct: nobody gets charged twice, and we never sell stock we don't have.
- Fast: placing an order should take a couple of seconds at most.
- Keeps running: a big sale shouldn't take checkout down.
- Clear: if something goes wrong, the customer should know whether they've been charged.
Rough numbers (5 minutes)
These are made up, but reasonable. Say 10 million orders a day, about 3 items each, and sale days that bring 10 times the normal traffic.
- ~120
- orders per second, on average
- ~1,200
- orders per second on a big sale day
- ~3,600
- stock updates per second at that peak
- ~50 GB
- of new order data a day
| Question | Rough math | What it means |
|---|---|---|
| How many payment calls? | One per order | About 1,200 a second at a peak, to an outside provider |
| How much order data? | 10 million orders × about 5 KB | About 50 GB a day, fine for a relational database |
| How bad is one hot item? | A deal everyone wants | Thousands of updates a second to one row |
The overall design (10 minutes)
- When the checkout page opens, the app makes up a unique order ID. Every retry of 'Place order' sends the same ID.
- The checkout service saves the order as pending.
- It holds the stock for each item, with a time limit.
- It charges the customer through the payment provider, passing along the same order ID.
- It marks the order as placed and puts an 'order placed' message on a queue for the warehouse, emails and so on.
What the apps need to do
| Action | Who does it | What's sent |
|---|---|---|
| Place an order | Customer | Cart items, address, payment method and the order ID |
| Check an order | Customer | Order ID, and we return its status |
| Payment result | Payment provider | A webhook saying whether a charge went through |
What we store and where
| Data | Where it goes | Why |
|---|---|---|
| Orders | Relational database | It's the permanent record, and it has to be correct |
| Stock counts | Relational database | Updates must never let the count go below zero |
| Stock holds | Relational database | Each one expires if the customer doesn't finish |
| Payment attempts | Relational database | So we always know what we asked the provider to do |
| Order placed messages | Queue | Other teams react without slowing checkout down |
Step 1: Never charge twice (10 minutes)
Customers double-click, phones lose signal, and requests get retried. That's why the order ID is made up front: if the same order ID comes in twice, we return the first result and don't start again. That's idempotency.
Before calling the payment provider, save a 'payment attempt' with the order ID. Most providers accept an idempotency key, so pass the order ID along. If we retry, they won't charge the card again.
The tricky case is a timeout, where we don't know if the charge went through. Don't just try again blind. Ask the provider what happened to that key, or wait for their webhook to tell us.
Step 2: Never sell what you don't have (10 minutes)
When an order starts, take the item out of stock with one update that only works if there's at least one left. If it fails, the item's sold out and we tell the customer right away. That's the consistency we need here.
Each hold expires after about 10 minutes. If the payment fails or the customer walks away, the stock goes back on its own.
What about a lightning deal?
If everyone wants the same item, thousands of updates hit one row and wait in line. One fix is to split that item's stock into several smaller counts and have each order take from one at random. That spreads the load, a lot like sharding the item.
Step 3: After the order's placed (10 minutes)
The warehouse, confirmation emails and analytics all care about new orders, but none of them should slow checkout down. Checkout puts an 'order placed' message on a message queue, and each of them reads it in its own time.
Save the message in the same database update as the order, and have a small job send it to the queue. That way an order can never exist without its message, even if the queue's down for a minute.
Choices you should be able to defend
| We chose | Instead of | Because | We'd reconsider if |
|---|---|---|---|
| Holding stock at checkout | Holding it when items go in the cart | Carts sit around for days and would lock up stock | Items sell out while people are still paying |
| Charging during checkout | Charging later, after the order's confirmed | The customer gets a clear answer straight away | The payment provider got slow enough to hurt checkout times |
| A relational database | A NoSQL store | We need updates that can't go wrong | Order volume outgrew what we can split up |
| Splitting hot stock counts | One count per item | One deal can't pile up on one row | Most items never get busy enough to need it |
What could go wrong (5 minutes)
| What happens | What we do about it |
|---|---|
| The payment provider times out | Check what happened using the order ID before doing anything else |
| The payment provider is down | Keep the order pending, hold the stock, and tell the customer we'll retry or ask them to try again |
| Checkout crashes after charging | A cleanup job finds charged orders that never got confirmed, and the provider's webhook fills in the gap |
| A sale overloads the stock database | Split hot items' counts, and let customers wait in a short virtual line |
| The queue goes down | Orders still go through. Their saved messages get sent once it's back |
Follow-up questions (5 minutes)
What about gift cards and split payments?
Treat each payment as its own attempt with its own key, and only confirm the order once they've all succeeded. If one fails, undo the others.
How would you handle cancellations?
Add a cancelled state, release any stock, and refund through the provider with a new idempotency key.
What if we sold in many countries?
Run checkout in several regions, but keep each item's stock in one place so two regions can't both sell the last one.
How the 60 minutes add up
| Minutes | What you're doing |
|---|---|
| 0–5 | Agree on what you're building |
| 5–10 | Rough numbers |
| 10–20 | Draw the overall design |
| 20–30 | Never charge twice |
| 30–40 | Never sell what you don't have |
| 40–50 | After the order's placed |
| 50–55 | What could go wrong |
| 55–60 | Follow-up questions |
Key concepts used here
- Idempotency and retriesMake sure a retried request doesn't happen twice.
- Consistency and the CAP theoremWhat happens when copies of your data can't agree.
- Message queues and event streamsKeep services from depending on each other, and handle traffic spikes.
- WebhooksHow one system tells another that something happened.
- Relational vs non-relational databasesPick a database based on how you'll actually use it.
- Sharding and consistent hashingSplit data across machines and keep it balanced.
Further reading
We wrote this sample answer ourselves.
Practice Loop is independent and isn't affiliated with or endorsed by Amazon.