Charge-Then-Save: Making a Checkout Handler Safe Under Retries
MercorSoftware Engineer · Technical ScreenLast updated
Review a ticket-checkout handler that bills a card through a remote gateway and then saves an order row, explain every way it breaks when calls fail or are retried, and redesign it. Tests reasoning about partial failure, idempotency, and reconciliation.
A concert-ticketing service exposes the handler below. Your mobile app calls it when a fan taps Buy. The app has a 10-second timeout and automatically retries once on timeout or on a 5xx response.
def buy_tickets(fan_id: str, show_id: str, seats: int, price_cents: int) -> dict:
total = seats * price_cents
receipt = gateway.bill(fan_id, total) # HTTPS call to card processor
orders.insert({ # one row in Postgres
"fan_id": fan_id,
"show_id": show_id,
"seats": seats,
"total": total,
"receipt": receipt,
})
return {"ok": True, "receipt": receipt}
Assume gateway.bill moves money and returns the processor's receipt ID, or raises on an error it can detect. orders.insert writes a single row. There is no other code involved.
This is a discussion round; you do not need to produce working code, but a short sketch of your redesign is welcome.
Part 1: Find the failures
Walk through the request's timeline. For every point where a process can crash, a connection can drop, or a response can be lost, describe what the fan's card and the orders table look like afterwards, and what the app's automatic retry does to that state.
Part 2: Explain why the obvious fix is not enough
A teammate proposes wrapping both lines in a database transaction. Explain what this does and does not protect.
Part 3: Redesign
Change the handler (and anything around it: schema, client contract, background jobs) so that:
- a fan is never billed twice for one tap of Buy, even with retries, crashes, and duplicate deliveries;
- every successful bill eventually has a matching order row;
- the app gets a truthful answer even when the outcome is not yet known.
State the assumptions you need from the card processor.
Constraints / things to clarify
- Does the processor accept a client-supplied idempotency key, or let you search charges by a reference you supply?
- Can the app generate an ID per purchase attempt and reuse it on retry?
- What should the fan see while a payment's outcome is unknown?
Hints
Hint 1
Draw a timeline with four checkpoints: before billing, during billing, between billing and insert, after insert but before the response reaches the phone.
Hint 2
A timeout on the network call does not mean the bill failed. What would you need in order to ask the processor later?
Hint 3
Write something durable *before* you move money, so a crash afterwards leaves a trace you can find.
Hint 4
Give each tap of Buy a unique ID chosen by the client, make it unique in your table, and pass it to the processor too.
Follow-up questions
- The processor has no idempotency keys and no search API. What can you still guarantee, and what would you change?
- How would you add refunds so that a refund retried three times only refunds once?
- Your reconciliation worker crashes halfway through a batch. Why is that safe with your design?
- Where would you put an outbox or message queue if order confirmation emails must go out exactly once?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.