Your payment service sends a request to the bank but the connection times out before receiving a response. You don't know whether the payment succeeded or failed. How would your system handle this?
How I frame it in the interview
This is the classic ambiguous outcome problem — the request may have reached the bank and succeeded, or never arrived, or is still processing. I'd state clearly: you cannot mark PAID or FAILED from a timeout alone. The system needs an explicit PAYMENT_UNKNOWN (or PROCESSING) state, idempotent recovery, and reconciliation until the truth is known.
What I would do — step by step
- Before calling the bank: Persist order as
PAYINGwith a uniqueidempotency_keyandcharge_referenceyou send to the gateway. - On timeout: Move to
PAYMENT_UNKNOWN. Return "Payment processing — we'll confirm shortly" to the user. Do not release inventory or show failure yet. - Recover truth (in parallel paths):
- Status API —
GET /payments/{ref}at gateway with same idempotency key - Safe retry — replay charge with same key; gateway returns original result if already processed
- Webhook — bank posts SUCCESS/FAILED asynchronously
- Reconciliation batch — nightly poll of all UNKNOWN in last 24–72h
- Status API —
- Terminal transition: Only move to
PAIDorFAILEDwhen gateway confirms. Notify user via email/push when final.
Your service Bank / gateway
| |
|-- POST /charge (idempotency K1)->|
| order PAYING | (processing…)
| << TCP timeout, no body >> | may have succeeded
| |
| order -> PAYMENT_UNKNOWN |
| return "processing" to user |
| |
|-- background: |
| 1) GET status(txn_ref, K1) -->| or wait webhook
| 2) retry charge(K1) -------->| same key = no double debit
| 3) reconciliation job ------>| poll unsettled txns
| |
|<- SUCCESS / FAILED ------------|
| order -> PAID or FAILED |
| notify user final state |Why idempotency key is non-negotiable here
Without it, your reconciliation retry looks like a second payment to the bank. With it, retry after timeout is safe: gateway either processes once or returns the stored outcome for key K1. Same key must be tied to order_id for the life of that payment attempt.
// Never flip to FAILED on timeout alone
ChargeResult result = paymentGateway.charge(request); // throws TimeoutException
catch (TimeoutException ex) {
order.markPaymentUnknown(chargeRef, idempotencyKey);
orderRepo.save(order);
return PaymentResponse.processing(orderId, "We're confirming with your bank…");
}
// Reconciliation worker (every 30s / via webhook)
void reconcile(Order order) {
GatewayStatus status = gateway.queryStatus(order.getChargeRef(), order.getIdempotencyKey());
if (status == SUCCESS) {
order.markPaid(status.getTxnId());
} else if (status == FAILED) {
order.markPaymentFailed(status.getReason());
} else if (status == NOT_FOUND && order.unknownSince() > Duration.ofMinutes(15)) {
// safe window passed — treat as failed OR escalate manual review
order.escalateReconciliation();
}
orderRepo.save(order);
}Walkthrough — user sees spinner, then email
User pays ₹50,000. Your service calls Razorpay/Stripe, waits 30s, socket dies. DB shows PAYMENT_UNKNOWN. UI: "Confirming payment…" (not failed). Background job queries status at T+45s — gateway says captured. Order → PAID, send confirmation. If gateway said not_found after 15 minutes, policy: mark failed and let user retry with a new idempotency key (new attempt).
Weak answers to avoid
"Mark payment failed and ask user to retry." Risk: first attempt actually succeeded → user pays twice or you ship without realizing money arrived.
"Retry immediately in a loop until success." Without idempotency and backoff, you double-charge or hammer the bank during an outage.
Follow-up questions
User closes the app while status is UNKNOWN — what do they see on return?
Order detail page polls GET /orders/{id}/payment-status or uses push. Show processing until terminal state. Never show "Try again" until FAILED is confirmed.
Webhook arrives SUCCESS but your DB already marked FAILED from a bug — how to detect?
Immutable payment event log + reconciliation alerts on state mismatch. Manual ops queue for PAID at gateway vs FAILED locally. Refund or fulfill based on gateway as source of truth for money movement.
How long do you keep UNKNOWN before auto-failing?
Depends on gateway SLA — often 15–30 min for UPI/card, up to 24h for some rails. Document policy per payment method. After window, fail only if status API returns definitive not_found; else escalate human review.
Does this relate to the double-click payment question?
Yes — same idempotency key ties together user retries, timeout recovery, and webhook. Double-click and timeout are both duplicate delivery of the same logical payment attempt.
What metrics do you alert on?
payment_unknown_count, unknown_age_p99, reconciliation lag, mismatch rate between gateway ledger and internal orders. Spike in UNKNOWN often means bank degradation.