Skip to content
Preptima
hardScenarioDesignMidSeniorStaff

A customer's bank shows the money left their account and you have no order. How does your checkout make that impossible, and how do you find the ones that already happened?

Write the order before you call the payment provider and key both with the same idempotency token, so a timeout leaves a record you can resolve. Then reconcile the provider's ledger against your orders daily in both directions, because a lost outcome is silent by default.

5 min readUpdated 2026-07-29Target archetype: Big Tech, Enterprise Captive, Product Startup
Practice answering out loud

What the interviewer is scoring

  • Does the candidate create the order record before calling the provider and key both with the same idempotency token
  • Whether a timeout is treated as an unknown outcome rather than a failure
  • That the outcome is expected from more than one channel, duplicated and out of order, and state transitions are idempotent
  • Whether a two-directional reconciliation against the provider's ledger is proposed as the safety net rather than retries
  • Does the answer decide what happens to an orphan - fulfil, refund, or ask - and who is accountable for that call

Answer

The gap is a boundary, not a bug

There are two systems of record: the payment provider's, which knows money moved, and yours, which knows what the money was for. Nothing spans them transactionally. Every occurrence of this failure is a moment where one of them committed and the other did not learn about it, and the reason it is so common is that the ways of not learning are all silent.

The concrete sequences are worth naming because each needs a different fix. Your request to the provider times out, the provider processed it, and your code takes the exception as a failure. The shopper is redirected to a bank for authentication, authorises successfully, and never lands back on your return URL — they close the tab, the mobile browser loses the session, the network drops. The provider's webhook is delivered to an endpoint that is deploying, returns a 500, and the retry schedule expires or the retries are dropped by something upstream. Or your own code succeeded at payment and then failed on the very next step, before the order was persisted.

sequenceDiagram
  participant S as Shopper
  participant C as Checkout
  participant P as Payment provider
  C->>P: Authorise with idempotency key
  P-->>C: Timeout - outcome unknown
  P->>C: Webhook authorised
  S->>C: Returns from bank redirect
  C->>P: Retry with same key

The point of interest is that the three inbound facts arrive in any order and may each arrive more than once, so whichever one lands first has to be able to complete the order alone.

Write the order before you take the money

The single design decision that removes most of this class is to persist the order, in a pending state, before making any call to the provider. It costs one write and it changes the nature of every subsequent failure: instead of a payment with nothing to attach it to, you have a record in a known-incomplete state that a reconciler can find and resolve. Abandoned pending orders are cheap to clean up. Orphaned money is not.

That record supplies the idempotency key. Generate it on the server when the order is created, send it with the authorisation request, and reuse the identical key on every retry of that attempt. A provider that honours idempotency will then return the original outcome rather than charging again, which converts your ambiguous timeout into a definite answer on the next attempt. Getting this wrong in the obvious way — generating a fresh key per HTTP attempt — is precisely how a retry storm becomes a set of duplicate charges, and it is the detail an interviewer will listen for.

The corollary is that a timeout is an unknown, not a failure. Code that catches a timeout and shows "payment failed" is asserting something it has no evidence for. The correct behaviour is to leave the attempt in an indeterminate state, resolve it actively by querying the provider for that key or reference, and only then tell the customer anything definitive. If you must say something immediately, say that you are confirming, and follow up.

Expect the outcome three times and in any order

Treat the redirect return, the webhook and your own polling as three independent sources for the same fact. Design so that each one alone is sufficient to advance the order, and so that all three arriving is harmless.

That means state transitions must be idempotent and guarded: moving from pending to authorised happens once, and a second notification of the same event is recognised by the provider's event identifier and discarded rather than applied. It means you must never rely on the browser coming back, because the browser is the least reliable of the three and belongs to the customer. It means the webhook endpoint has to be as available as your checkout, verify its signature, acknowledge quickly and process asynchronously, and be safe to receive events out of order — a capture notification can genuinely arrive before the authorisation notification it depends on, and the handler should park it rather than reject it. And it means a sweeper: a job that finds pending attempts older than a short threshold and asks the provider what happened, because active resolution is the only thing that closes the loop when all three channels have failed.

Reconcile in both directions, daily

Prevention narrows the window and does not close it, so the safety net is reconciliation against the provider's own ledger — the transaction reports and settlement files, not your copy of what you think you sent. Pull them on a schedule and match every line to an order.

Two exception sets fall out, and both matter. Provider transactions with no matching order are the orphans in the question: money taken with nothing behind it. Orders marked paid with no matching provider transaction are the opposite error, where you have shipped goods against a payment that does not exist, and that set is usually smaller and more expensive per item. A third check, further downstream, matches the provider's settlement to what actually arrived in the bank account, which is where fee and chargeback discrepancies surface.

The output has to be a worklist with an owner and an ageing measure, not a report. The number to publish is the count of unmatched items over the age threshold, because it is the only figure that tells you whether the control is working. Retaining the raw provider reports alongside your own records is what makes an individual customer's dispute answerable months later.

Deciding what to do with an orphan

Finding one is not resolving it, and the resolution is a commercial decision that should be pre-agreed rather than improvised. There are only three outcomes. You can construct the order and fulfil it, which is right when you can determine with confidence what was being bought, from a pending order or a basket snapshot, and the goods are available. You can return the money, which is the safe default when you cannot. Or you can contact the customer, which is honest and slow and worth reserving for high-value cases.

Two mechanics separate the credible answer here. An authorisation that was never captured is voided rather than refunded, and the effect on the customer's available balance differs in visibility and timing between the two, which is exactly what they will be asking about. And speed dominates goodwill: a refund the same day is an apology, and the same refund a fortnight later is a complaint that has already been escalated, possibly as a dispute you will lose on evidence. That is the strongest argument for the daily cadence rather than a monthly one.

It is also worth saying which failure you would rather have. Money with no order costs you trust and a refund. An order with no money costs you the goods. Both are worth engineering against, and a system that only reconciles one direction has decided which one it prefers without noticing.

A timeout tells you nothing about whether the money moved. Persist the order first, carry one idempotency key through every retry, accept the outcome from whichever channel arrives first, and let a two-way reconciliation catch what all of that still misses.

Likely follow-ups

  • The provider's webhook arrives before your redirect handler runs. Does your state machine cope?
  • How do you distinguish a genuine duplicate charge from two legitimate orders placed a minute apart?
  • An orphaned authorisation is found four days later. Do you void it, capture it, or fulfil the order?
  • What is the reverse case - an order with no payment - and which of the two would you rather have?

Related questions

payment-orchestrationidempotencywebhooksreconciliationorphaned-payments