Katkat

Charge-Then-Save: Making a Checkout Handler Safe Under Retries

MercorSoftware Engineer · Technical Screen

Last 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?