← Home
💬Real-life Scenario-based Interview QuestionsInterview QuestionsNEW
Must Know
Experience levelSenior · 5+ YOESenior / staff depth expected

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?

PaymentsDistributed SystemsIdempotencyReconciliation

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

  1. Before calling the bank: Persist order as PAYING with a unique idempotency_key and charge_reference you send to the gateway.
  2. On timeout: Move to PAYMENT_UNKNOWN. Return "Payment processing — we'll confirm shortly" to the user. Do not release inventory or show failure yet.
  3. 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
  4. Terminal transition: Only move to PAID or FAILED when gateway confirms. Notify user via email/push when final.
Timeout = unknown outcome → never guess
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.