Design a shopping app with cart and checkout: catalogue, cart merging, price authority, payment, idempotency and recovery from interruptions.
Predict first
A user double-taps Pay and the request is retried after a timeout, all with the same order key, then buys another cart. How many orders and what is charged, compared with a new key for each attempt?
final class PaymentServer {
private var orders: [String: String] = [:]
private(set) var chargedCents = 0
func placeOrder(key: String, cents: Int) -> String {
if let existing = orders[key] { return existing }
let id = "order-\(orders.count + 1)"
orders[key] = id
chargedCents += cents
return id
}
}
let withKey = PaymentServer()
let taps = [withKey.placeOrder(key: "cart-7a", cents: 2000),
withKey.placeOrder(key: "cart-7a", cents: 2000),
withKey.placeOrder(key: "cart-7a", cents: 2000),
withKey.placeOrder(key: "cart-8", cents: 500)]
print(taps.joined(separator: " "), "charged:", withKey.chargedCents)
let withoutKey = PaymentServer()
for attempt in 1...3 { _ = withoutKey.placeOrder(key: "attempt-\(attempt)", cents: 2000) }
print("without a stable key charged:", withoutKey.chargedCents)
- order-1 three times then order-2, charged 2500; without a stable key, 6000
- Four orders, charged 6500
- Every attempt is rejected
Mental model. Checkout is a bank transfer slip with a reference number. If you submit the same slip three times the bank pays once, because it recognises the number.
Requirements and invariants. Browse and search a catalogue, keep a cart across sessions and devices, pay securely, and see order status. Invariants: never charge twice, the price charged is the price shown (or the user is told it changed), card data never touches your servers or logs, and an interrupted checkout always recovers to a known state. Catalogue: server-side search and filters with cursor pagination (Q318), cached images (Q327) and short-lived caches for prices and stock (Q338, Q319), because stale price and availability must be corrected at checkout, never trusted. Cart: stored locally for instant UI and for guests, with the server as the authority once signed in; on login merge the guest cart into the account cart by item with a deterministic rule (sum quantities, cap by stock) and show the result. Price authority: the client displays; the server computes totals (tax, shipping, discounts) and returns a quote with an expiry, and the client submits the quote id, so a changed price is detected instead of silently charged.
Payment. For physical goods use Apple Pay with PassKit (Q382) or a payment provider’s SDK that tokenises the card on device, so card numbers never reach your systems and your compliance scope stays small; for digital goods and subscriptions consumed in the app, App Review requires in-app purchase (Q381, Q484). Idempotency: generate one order key when the user starts checkout and send it on every attempt, so a double tap, a timeout and a retry produce one order and one charge, as the check below shows (without a stable key, three attempts charge three times). Model checkout as a state machine (quoted → paying → confirmed / failed) persisted locally. Interruptions: if the app is killed after the payment sheet, on the next launch fetch the order by key from the server (the server is the source of truth) instead of assuming failure, and use push or polling for status updates (Q375). Cross-cutting: accessibility of the pay flow (Q477), localisation of currency, names and addresses (Q473, Q475), experiments on the product page (see the analytics question) without touching the payment path, and funnel metrics (view → cart → checkout → paid) with a payment-success alert. Name the trade-off: stronger client validation improves speed but the server must still re-check everything.
A stable key makes retries replays of the same order; a fresh key per attempt creates a new charge each time.
Common traps
- A new idempotency key for each retry. Every retry becomes a new order and a new charge.
- Trusting the client’s totals. Prices, tax and discounts are computed and verified on the server.
They might follow up with
- What if the app dies mid-payment? On relaunch, look up the order by its key on the server and show its real status.
- How do you keep card data out of your systems? Tokenise on device with Apple Pay or a provider SDK and send only the token.
Related questions
- 26.1.1 How do you approach a mobile system design question: what do you clarify, in what order do you design, and…
- 26.1.2 Design a messaging client: transport, ordering, delivery states, offline sending, media, notifications and…
- 26.2.1 How do you design a client for reliability: deadlines, circuit breakers, degradation tiers, bulkheads, kill…
- 26.2.3 How do you design analytics and experimentation that respect privacy: event schemas, consent, minimisation…
- 12.5.1 How should a client retry failed requests: what to retry, how long to wait, and what idempotency has to do…
- 17.4.2 How does StoreKit 2 work: products, purchases, transaction verification, entitlements, refunds, and what must…
- 17.4.3 How do Apple Pay and Wallet passes work with PassKit: payment requests, merchant capabilities, tokens and…
- 25.3.2 How does App Review work, what causes common rejections, and what are export compliance, the privacy labels…
← Design an offline-first reader app: content model, sync, storage…