Design Checkout
- Scope checkout as a multi-step flow where correctness under retries beats raw throughput
- Walk the complete-checkout path end to end, all keyed on one checkout token
- Work the failure modes: a double submit, a race for the last unit, a crash mid-capture
Understanding the problem
Design checkout for a merchant's store: the flow from a filled cart to a completed order. When a buyer hits "Complete order," reserve the inventory, capture the payment, and create the order exactly once, even if the request is retried or the button is clicked twice.
Checkout is the moment a merchant on Shopify actually gets paid, so it is the highest-stakes path on the platform. A buyer fills a cart on a storefront, enters shipping and card details, and clicks "Complete order." Behind that one click, several things have to happen together. The store has to hold the stock so it isn't sold out from under the buyer. The card has to be charged, and an order has to be created that the merchant will fulfill. If any of those runs twice, or runs partway and stops, someone loses money or a promise. This lesson is about making that whole sequence correct when the network is unreliable and buyers are impatient.
The trouble is that a checkout is not one action, it is a chain of them, and the chain crosses systems Shopify does not fully control. Reserving stock is a database write. Charging the card is a call out to a payment provider that can be slow, can fail, and cannot be taken back with a database rollback. Buyers double-click when a page hangs, and mobile apps resend a request whose response got lost. So the real design question is not "how fast." It is "how do we run this multi-step chain so a retry, a double-submit, or a mid-flow crash never double-charges a buyer?" The same chain must never oversell the merchant's stock or leave a paid order that does not exist. Frame it that way early. It shapes every decision that follows.
Functional requirements
Keep the scope tight. The depth here is one flow done correctly. The core is four things that must happen together and exactly once per checkout. Hold the inventory so the buyer's items cannot be oversold. Charge the card. Create the order the merchant fulfills. And make a repeat of that same "Complete order" request return the original result instead of running again. Everything else sits below the line.
In scope
- Create a checkout from a cart and compute the total (items, shipping, tax) in integer subunits
- Reserve inventory so a merchant never oversells a variant, even under concurrent buyers
- Authorize and capture payment through an external provider, safe to retry
- Create the order exactly once, keyed on the checkout token, and replay a duplicate submit
Below the line (name it, then skip)
- The storefront rendering that shows the cart (its own read-heavy design, split out of the monolith)
- Merchant payout and settlement timing (a separate downstream flow)
- Fraud scoring and risk rules (a real gate at Shopify, but its own subsystem)
- Tax and shipping-rate calculation internals (treated as functions we call)
- Refunds, cancellations, and partial fulfillment (each a later state transition on the order)
Non-functional requirements
These are the requirements that decide the design, so reason through each. Correctness comes first and it splits into two promises. Money correctness: one "Complete order" click charges the card exactly once and creates exactly one order, even under concurrent retries and a crash mid-flow. Inventory correctness: available stock for a variant must never go negative, which is the requirement Shopify states almost word for word in its own design prompts. Availability sits just behind correctness. Checkout has to stay up during the largest coordinated spike in retail, Black Friday and Cyber Monday (BFCM). Forced to choose, we'd rather make a buyer retry than sell the same last unit twice.
Scale is the requirement we relax on purpose, with one sharp exception. Completed checkouts are a modest write volume next to storefront page views, which run orders of magnitude higher. So raw checkout throughput is not what threatens the design. The exception is contention. During a flash sale, tens of thousands of buyers aim at the same store, sometimes the same single product, and fight over the last few units of one inventory row. That hot-row contention, not aggregate queries-per-second, is the hard part, and it is why the inventory deep dive is the center of the lesson. Hold every money amount as an integer count of the currency's smallest unit, cents for US dollars, never a floating-point number, because a ledger that loses pennies to rounding is a ledger with bugs.
- Exactly 1 Order per checkout. Even under double-submits and retries.
- Never < 0 Inventory available. No oversell under concurrent buyers.
- Exactly 1 Card capture. One charge per completed checkout.
- Integer subunits Money type. Cents, never a float.
The set-up
Core entities
Name the objects the flow moves around before drawing anything. Four carry the design:
Checkout
The session that turns a cart into an order. It holds a unique checkout_token, the shop_id it belongs to, the line items and computed totals in integer subunits, and a state: open, processing, completed, or failed. The token is the idempotency key for the whole flow.
InventoryReservation
A short-lived hold on N units of a specific variant at a specific location, tied to one checkout. It lets a buyer keep items while paying, and it expires on a TTL so an abandoned checkout releases the stock instead of freezing it forever.
Order
Created once, when payment is captured: an id, the shop_id, the checkout_token it came from (unique), the amount in integer subunits, the line items, and a fulfillment status. One order per checkout token.
Payment
The record of the money movement and its progress through the multi-step capture. It holds a card-vault token (never the raw card), a recovery_point marking how far the flow got, the provider result, and the stored final response.
The store under all of this is MySQL, sharded by shop_id, and that choice is load-bearing rather than incidental, so be ready to defend it. Shopify partitions the whole platform into pods, where each pod is an isolated set of MySQL clusters holding a slice of shops, and a request is routed to its shop's pod up front. Two things fall out. First, one merchant's flash sale lands on that merchant's pod and cannot starve every other store, the noisy-neighbor problem solved by isolation. Second, a relational store gives us the exact guarantees checkout needs. A unique constraint on checkout_token means two concurrent submits of the same checkout can never both create an order, and a conditional row update makes an inventory decrement atomic. An eventually-consistent key-value store cannot promise either, because two replicas can each accept a write and only discover the conflict later, after both have already acted.
API
Two calls carry the flow. Creating a checkout from a cart returns a checkout_token, generated once and reused on every retry of the completion. Completing the checkout is idempotent on that token: the first call runs the full sequence, and any later call with the same token returns the order that was already created rather than starting over. The interesting behavior is in the responses to a completion, so the deep dives build each one.
Five responses, and the last three are the ones the round is really about. A first completion runs the flow and returns 200 with a new order. A replay with the same token returns that same order from storage, no second charge. A second submit arriving while the first is still processing returns 409, telling the client to back off and retry. And a completion that cannot reserve stock returns 422 before any card is touched. The deep dives build each of these.
High-level design
Walk the complete-checkout path one requirement at a time, and build the simplest version that works before hardening it. Each box is tinted by its job. Blue for the Buyer, violet for the Checkout service, and amber for the one slow external call, the Payment gateway. Green marks the stores it commits to: the Inventory store, the Order store, and the Card vault. The walkthrough holds to the same scheme.
1) Receive the completion and reserve inventory. The Buyer clicks "Complete order," and the Checkout service does not charge anything yet. First it runs one indexed lookup of the checkout_token in the Order store, which costs well under a millisecond. If an order already exists for this token, the work is done: it returns the stored order and never touches the Payment gateway. If not, it moves the checkout to processing and reserves each line item in the Inventory store with a conditional decrement that only succeeds when enough units remain. If any item cannot be reserved, it releases whatever it took and returns 422 before any card work. What we are punting: two buyers reserving the last unit at the same instant need a precise rule for who wins. Deep dive two settles that, and it is the heart of the design.
2) Tokenize the card and authorize payment. With stock held, the Checkout service hands the raw card straight to the Card vault and gets back an opaque token. The vault encrypts and tokenizes the card so the sensitive data never enters the checkout code. It then asks the Payment gateway to authorize that token for the checkout total. This round trip runs hundreds of milliseconds to a couple of seconds, orders of magnitude slower than the database writes around it. It is the dangerous step, because it moves money in a system we do not control and cannot undo with a rollback. So it runs outside any open database transaction, carrying its own idempotency token, the checkout id, so the gateway recognizes a resubmission. The punt here is the scary one: if we crash after the gateway authorized but before we recorded it, was the buyer charged. Deep dive three works through exactly that.
3) Create the order and capture the payment. When authorization succeeds, the Checkout service captures the funds. Then, in one short database transaction against the Order store, it inserts the Order row keyed on the unique checkout_token and advances the payment's recovery point past the capture. The unique constraint is doing real work: if a second copy of this completion is racing us, only one insert can win, and the loser learns immediately that an order already exists. The captured amount is stored in integer subunits, so no float ever enters the books. This is the step that turns a paid card into a fulfillable promise for the merchant. What we punt on: a duplicate submit that arrives at this exact moment needs a clean answer, either the stored order or a 409. Deep dive one settles that.
4) Commit the stock, store the response, confirm. With the order written, the Checkout service converts the reservations in the Inventory store into a permanent decrement, so the units are now sold rather than merely held. It marks the checkout completed and stores the final response, the order id and status, on the checkout record. That stored response is what every later retry with this token replays, a sub-millisecond read that never re-runs the flow. Then it fires an order-confirmation event to a background worker for the buyer email and the merchant notification, off the request path so a slow email never delays the checkout. The last thing to handle is the operational reality of BFCM: a card vault or gateway that wobbles under load, and reservations from abandoned checkouts that must be released. Deep dive four covers both.
Sizing the checkout
A payments-shaped round doesn't open with queries-per-second. Once correctness is settled, though, a senior candidate puts the numbers on the board, because they explain why the hard part is contention and not throughput. Start from a normal day. Say the platform moves on the order of a billion dollars of orders a day at an average order around 90 dollars. That's roughly 10 million completed checkouts a day, or a bit over 100 checkouts a second on average. Each completion fans into a handful of small writes: the reservation, the order row, the capture record, and the committed decrement. So the steady-state write load is low hundreds of row writes a second, comfortable for the pod's MySQL primary without any special effort.
Now BFCM. Shopify has reported platform peaks around 32 million requests per minute, which is roughly 530,000 requests a second, but the overwhelming majority of that is storefront reads (browsing, cart views), not completions. Checkout completions spike too, into the tens of thousands a second across all stores at the very peak. Call it a 100x multiplier over the average, and still a small fraction of total traffic. The number that actually hurts is narrower still. A single hyped product on one store can pull thousands of concurrent buyers at the same inventory row, all trying to grab the last units in the same second. That is not a throughput problem you cache your way out of, it is a write-contention problem on one row, which is exactly what the inventory deep dive is built to survive.
- Steady state: ~100 checkouts/s. About 10M completed checkouts a day. Each fans into a handful of small row writes, low hundreds of writes a second, easy for the pod primary.
- BFCM peak: ~32M req/min. Roughly 530K requests a second platform-wide, but mostly storefront reads. Completions spike to tens of thousands a second, a ~100x multiplier over average.
- The real bottleneck: one hot variant. A flash sale points thousands of concurrent buyers at a single inventory row. The design must survive contention on that row.
- Replay cost: 1 indexed read. A retry that hits a completed checkout_token is a sub-millisecond point lookup returning the stored order. No card call, so replays are effectively free.
Technology choices
A checkout round interrogates every dependency you put on the board, so treat each component as a claim you have to defend. Name a concrete pick, say why that category fits from first principles, and keep ready the failure the interviewer will probe.
- The order and checkout store is MySQL, sharded by
shop_idinto pods. We reach for a relational store here for two guarantees checkout depends on. A unique constraint oncheckout_tokenlets two concurrent submits create at most one order. A real transaction makes the order row and the payment recovery point commit together or not at all. Both come for free with a strongly-consistent store and neither is safe on an eventually-consistent one, where two replicas can each accept the same token and reconcile after both have already charged. Sharding byshop_idkeeps a merchant's data on its own pod, so a flash sale on one store cannot exhaust the primary that every other store shares. The probe is "why not a key-value store like DynamoDB?" and the answer is the constraint plus the transaction. - The inventory store is a MySQL row per variant per location with a conditional decrement,
UPDATE ... SET available = available - :qty WHERE available >= :qty, so the database itself refuses to let stock go negative and the update reports whether it applied. Reservation rows carry a TTL so an abandoned checkout releases its hold. We keep it relational instead of a fast counter in Redis. Oversell is a correctness bug that a cache's eventual consistency would let through, and the write volume is low enough that strong consistency costs us nothing. The probe is "what happens when thousands of buyers hit the last unit at once?" That single hot row is deep dive two, where the conditional update makes exactly one of them win and the rest get a clean 422. - The card vault is an isolated tokenization service (CardSink on write, CardServer on read) that sits outside the main application. Raw card numbers are encrypted and swapped for an opaque token the instant they arrive, so the PCI-sensitive data never touches checkout code and the compliance boundary stays small. The checkout service only ever handles the token. The probe is "what if the vault is unavailable at peak?" Then the checkout cannot proceed to authorization and fails fast with a retryable error rather than blocking, and the vault gets the same pod-style isolation and capacity headroom as any other checkout dependency.
- The payment gateway is an external provider we cannot roll back, wrapped in a circuit breaker (Semian). No database
ROLLBACKun-charges a card, so we treat the call as a foreign state mutation. Run it outside any open transaction, and pass our own idempotency token so a resubmission captures once. Put a circuit breaker in front: when the provider slows or fails, calls trip open and return fast instead of piling up and dragging every checkout down with them. The probe is "what if the gateway is down during BFCM?" The breaker sheds those checkouts quickly and cleanly, and a background reconciliation job later squares our captures against the provider's settlement record to catch any disagreement.
Potential deep dives
The grading concentrates here. The interviewer names a failure mode and pushes on it, because staying correct when things break is what the round checks. These are the ones a Shopify checkout design leans on, each an escalation the panel drives.
1) The buyer double-clicks "Complete order." How do we not make two orders?
A hung page makes buyers click again, and a mobile app resends a completion whose response was lost. So two copies of the same checkout completion can reach the service at the same instant. This is the race the design has to survive: both look up the token, both see no order yet, and if nothing stops them, both charge the card and create two orders. The fix has to make exactly one win and give the other a safe answer. Work up to it in tiers, because the obvious version has a hole.
Approach
The service runs "is there an order for this checkout token?" and, if not, charges the card and inserts the order. Read, then write, in two separate steps.
Challenges
There is a gap between the check and the write. Two concurrent completions both run the check, both see nothing, and both proceed to charge and insert. This is a time-of-check-to-time-of-use race, where the world changes between the moment you look and the moment you act, and it produces exactly the double charge and double order we are trying to prevent. Nobody should ship this, but being able to name why it fails is the point of the question.
Approach
Drop the separate check. The token is generated once per checkout and reused on every retry, so make it a unique key on the order row. Both completions try to INSERT an order for the same checkout_token, and the unique constraint lets exactly one succeed. The database, not the application, picks the winner, atomically, with no gap to race through. The loser's insert fails with a duplicate-key error, which is now a signal rather than a bug: it means an order for this checkout already exists or is being created.
Challenges
Knowing it lost is only half the answer. The loser still owes its client a response, and the right one depends on whether the winner has finished. If the order is written, the loser returns that same order, a clean replay. If the winner is still mid-capture, the loser cannot invent an order and must not start its own charge. We need a way for the loser to defer to the in-flight winner rather than guess.
Approach
Pair the constraint with the checkout's own state so the loser does the right thing every time. Before touching the gateway, the winner moves the checkout row to processing under a row lock (SELECT ... FOR UPDATE, which makes other transactions wait on that row). A concurrent completion that finds the checkout already processing returns 409 immediately, telling the client to retry shortly, rather than blocking a connection across a multi-second card call. Once the winner writes the order and marks the checkout completed, any later submit reads the stored order and replays it. Three outcomes, all safe: the winner charges once, an in-flight duplicate gets a 409, and a finished duplicate replays the stored order. The constraint guarantees one order; the state tells every other submit exactly what to do.
Challenges
You choose between making the loser wait on the lock or returning 409 fast. For checkout the fast 409 is better, because holding a database connection open across a card-network call is how connection pools drain during a spike. A 409 needs a client that retries it, which a storefront and the mobile SDK both do, but state that dependency. And a crash can leave a checkout stuck in processing, so the state carries a short lease that a later attempt can reclaim, which is exactly what deep dive three builds on.
The handling contention lesson covers unique constraints, row locks, and the time-of-check race in general.
Idempotency-Key mechanics directly and say so. What makes the Shopify version its own problem is that the token has to make the entire sequence idempotent, reservation, capture, and order creation together, not one write. The checkout token is that key. Put it on the completion path before anyone asks, and defend the strongly-consistent relational store over a key-value one, and it reads like you have shipped checkout code under load.2) Two buyers race for the last unit. How does inventory never go negative?
This is the correctness core of a Shopify checkout, and the requirement is almost verbatim from Shopify's own prompts: inventory should never go negative, overselling is not allowed. During a flash sale, thousands of buyers can hit the same variant in the same second, and a merchant who oversells a limited drop has to cancel real orders and burn buyer trust. The available count lives in one row, and that one row is the hottest write in the system. Work up the concurrency in tiers.
Approach
Read available for the variant, check it is at least the quantity wanted, then write back available - quantity. It reads like the obvious thing to do.
Challenges
It is the same time-of-check-to-time-of-use race as before, now on stock. Two buyers both read available = 1, both see enough, and both write 0 after each sells a unit, so two orders ship for one item in stock. Under a flash sale this does not just happen occasionally, it happens constantly, because hundreds of reads land between any read and its matching write. Overselling is the exact failure the requirement forbids.
Approach
Never read-then-write. Do it in a single statement the database applies atomically: UPDATE inventory SET available = available - :qty WHERE variant_id = :v AND available >= :qty. The WHERE available >= :qty guard means the row only changes when enough stock remains. The database serializes concurrent updates to the same row, so exactly the right number of buyers succeed and the rest get zero rows affected. A zero-rows result is the signal that this buyer missed the last unit, and the completion returns 422 before any card is charged. Available can never cross below zero because the guard is part of the same atomic write.
Challenges
A hard decrement at "Complete order" is correct but too late in the flow. A buyer who spends two minutes entering a card, only to hear at capture that the item just sold out, has a bad experience, and the merchant loses a sale. Checkout wants to hold the stock the moment the buyer commits, while they pay, and release it if they wander off. A raw decrement has no notion of a hold that expires. And this is a single-location count, while real merchants stock the same variant across multiple locations.
Approach
Split the count into on-hand and reserved. When a completion starts, atomically move units from available into a reservation row tied to the checkout, using the same conditional decrement so it still cannot oversell. The reservation carries a TTL, a few minutes, so if the buyer abandons the checkout a background sweeper returns the units to available. When payment captures, the reservation is committed: the units leave inventory for good and the reservation row is retired. If capture fails or the TTL lapses first, the hold is released and the stock is sellable again. For a multi-location merchant, availability is the sum across locations, and the reservation targets a chosen location's row, so two locations never contend on the same counter. The atomic guard guarantees no oversell; the TTL keeps abandoned carts from freezing stock; committing on capture ties the physical decrement to the money moving.
Challenges
The TTL is a tradeoff: too short and a slow-paying buyer loses their hold mid-checkout, too long and a hyped item looks sold out while reservations sit unpaid. During a flash sale you tune it short and lean on the sweeper. And the sweeper has to be idempotent, releasing a reservation exactly once, so a committed reservation is never released and an expired one is never released twice. This reservation-and-release shape rhymes with Amazon's seat or room booking, but pointed at multi-location merchant stock under flash-sale contention rather than a fixed set of seats.
The handling contention lesson generalizes the atomic-decrement pattern, and async processing covers the TTL sweeper that releases abandoned holds.
3) The server dies after the card is charged but before the order exists. Now what?
A payment-gateway call cannot be taken back. It changes the world in a system Shopify does not own, and no database ROLLBACK reaches it, which is what makes checkout genuinely hard. Picture the checkout service dying right after the gateway captured the card but before the order was written. The database says processing. The buyer's statement says charged. And there is no order for the merchant to fulfill. A retry must not charge the card a second time, and it still has to finish the work the dead request left half-done. This is exactly the problem Shopify built its Resumption library to solve.
The fix is to model the checkout as a state machine that advances through recorded checkpoints, and to make each external step safe to repeat. Break the flow into phases, each ending at a recovery point persisted on the payment record: reserve stock, then authorize, then capture-and-create-order, then commit-and-store-response. The gateway call lives between transactions, never inside one, because it is slow and external. On a retry, the service reads the recovery point and resumes from the last completed phase instead of starting over. That single marker is what turns a crash at any point into a safe resume.
Approach
Open a database transaction, reserve stock, call the gateway, write the order, and commit, all as one atomic unit. It feels clean: either the whole checkout happens or none of it does.
Challenges
It breaks two ways. First, the transaction stays open across a multi-second gateway call, so every in-flight checkout pins a database connection for the full round trip. Under a BFCM spike the connection pool drains and the primary stalls for everyone on the pod. Second, the atomicity is a lie. A ROLLBACK undoes the database writes but cannot un-charge the card, because the gateway never heard of your transaction. So a rollback after the card was captured leaves the money moved and no order recorded, the exact half-done state we are trying to avoid. The thing the big transaction was supposed to guarantee is the one thing it cannot.
Approach
Keep the database transactions short and put the gateway call between them. Phase one writes the reservation and sets the recovery point to reserved. The gateway call runs with no transaction held and carries the checkout id as its idempotency token, so the provider itself dedupes a resubmission. Ask it twice with the same token and it captures once, returning the same answer the second time. Phase two, in a short transaction, records the capture, inserts the order keyed on the checkout token, and advances the recovery point to captured. Phase three commits the stock and stores the final response. A retry reads the recovery point and resumes precisely. At reserved it re-submits to the gateway (safe, because the call is idempotent) or queries it for that token's status. At captured it skips straight to storing the response. This is what Resumption does, record progress through a multi-step payment action so retries are exactly-once.
Challenges
It leans on the gateway honoring an idempotency token, which real providers and Shopify's own payment abstractions do, so state that as the assumption it is. The recovery point must be durable before the gateway call, or a crash loses the fact that we even started charging. The processing lease from deep dive one needs a timeout so a crashed checkout can be reclaimed instead of frozen. And a slower reconciliation job squares our captures against the provider's settlement file to catch the rare case where our record and theirs disagree. The payoff: no single crash, at any point, double-charges the buyer or loses the order.
The async processing lesson generalizes resuming a multi-step operation after a failure.
4) BFCM: the vault wobbles, the gateway slows, holds pile up. How does checkout stay up?
Black Friday and Cyber Monday are the largest coordinated write spike in retail, and checkout has to keep taking orders through it while its slow dependencies strain. Three operational failures show up together at peak, and a strong answer handles all three without letting any one of them cascade into a full outage.
The first is a slow or failing payment gateway. Wrap every gateway call in a circuit breaker, which Shopify runs as its Semian library. Once a dependency's error rate or latency crosses a threshold, the breaker trips open, and calls to it return fast with a retryable error instead of piling up. Without it, a provider that slows to five seconds a call would hold every checkout's connection open until the whole pod runs out of connections and unrelated checkouts fail too. Failing fast on the sick dependency keeps the healthy path alive. The second is the card vault. Because CardSink and CardServer are isolated from the main app, a vault hiccup degrades to a fast retryable error on new checkouts rather than corrupting the checkout code path. It scales with the same pod headroom as everything else.
The third is reservations from abandoned checkouts. At a 100x spike, buyers start far more checkouts than they finish, and every unfinished one holds stock. If those holds are not released promptly, a hot product looks sold out while units sit reserved by buyers who already left. The TTL sweeper from deep dive two is the safety valve: it releases expired holds back to available, and during a flash sale you tune the TTL short so stock recirculates fast. The whole picture is graceful degradation: serve a degraded-but-correct checkout over a 500. Shopify load-tests for that posture every week against benchmark stores with its Genghis harness, so BFCM isn't the first time the system meets that load.
What is expected at each level
That is a lot of surface for one flow. Here's how much you need to land, by the level you're interviewing for. Keep in mind Shopify runs system design mainly for senior and staff candidates, and sometimes folds it into the escalating pairing round instead of a standalone hour.
Mid-level
Mostly breadth. Get to a working happy path and one retry. That means a checkout token created once, and a completion that reserves stock, charges the card, and creates an order. A repeat completion returns the same order instead of running again. You should explain why the checkout token has to be idempotent for the whole flow and why money is held in integer subunits. The interviewer will drive the deeper races. The bar: a clean account of how one token makes one completion safe to retry, even if the concurrency and crash cases need prompting.
Senior
Breadth, then real depth on the two races. You should reach the double-submit answer on your own: the unique constraint on checkout_token plus a processing state that returns a 409 or a clean replay. And you should reach the inventory answer on your own: the atomic conditional decrement that makes oversell impossible, with reservations and a TTL for held stock. Defend the sharded relational store over a key-value one without being asked, and tie the never-oversell requirement back to the merchant's trust. The bar: you find both concurrency races yourself, resolve each precisely, and justify the strongly-consistent store as a deliberate choice.
Staff+
Real depth on the hard failures, and the scope widens. You lead on the crash-mid-capture case: the resumable state machine with recovery points, why a transaction cannot wrap the gateway call, and an idempotency token pushed down into the provider. You raise the operational picture unprompted, circuit breaking on the gateway, card-vault isolation, the TTL sweeper for abandoned holds, graceful degradation at BFCM, and reconciliation against the provider's settlement record. You treat the multi-step, cross-system correctness as the center of the design rather than a corner of it, and your reasoning holds end to end while the interviewer redirects more than rescues. To clear the bar, the crash case and the hot-row contention are where you spend your depth, and you connect both back to what a merchant experiences.
Key Takeaways
- ✓Checkout is a multi-step, cross-system flow. Model it as a state machine and make the whole sequence, reserve then capture then create-order, idempotent on one checkout token, so a double-submit or a retry produces exactly one order.
- ✓Inventory never goes negative because the decrement is a single atomic conditional update in the relational store, not a read-then-write and not a cache counter. Reservations with a TTL hold stock while a buyer pays and release it if they leave.
- ✓The gateway call is a foreign state mutation a database rollback cannot undo. Keep it between short transactions, carry an idempotency token so a resumed capture charges once, and record recovery points (Shopify's Resumption) so a crash mid-capture resumes safely.
- ✓At BFCM the threat is contention on one hot inventory row, not aggregate QPS. Lean on isolation: pods per merchant, a circuit breaker on the gateway, an isolated card vault, and a short reservation TTL. Hold money in integer subunits, never floats.
Shopify interview guideKnow what Shopify asks before you walk in.
26 coding · 5 system design · 21 behavioral questions
Get their questions first.
InterviewLegend is independent and not affiliated with or endorsed by any company it covers.