Requirements & API: Payment Gateway

What an interviewer expects you to nail down before drawing a single box.

Functional

  • Accept a payment event after checkout, run a risk check, and execute one or more payment orders through a PSP.
  • Guarantee that retrying the same payment order (same idempotency key) never charges the card twice.
  • Record every money movement in a double-entry ledger and track each seller's balance in a wallet.
  • Reconcile internal records against the PSP/bank settlement file and surface mismatches for correction.

Non-functional

  • Exactly-once side effects: a retry must never produce a duplicate charge. Idempotency is required, not optional.
  • Strong consistency on the ledger: debits must always equal credits, with no eventual consistency on money.
  • Resilience to partial failure: failed payment orders retry, and poison messages end up in a dead letter queue.
  • Auditable and reproducible: every state change is recorded so the whole history can be replayed and reconciled.

API contract

POST /v1/payments { buyer, checkout_id, payment_orders[], idempotency_key } → { payment_id, status }
checkout_id dedups the event; each payment order carries its own idempotency key to the PSP.
GET /v1/payments/{id} → { status }
Execution status of a payment and its orders (NOT_STARTED, EXECUTING, SUCCESS, FAILED).
POST /v1/refunds { payment_order_id, amount? } → { refund_id, status }
Also idempotent; reverses through the same PSP.
(internal) reconcile(settlement_file) → { matched, mismatches[] }
Nightly job comparing the PSP/bank settlement file against the ledger.

About Payment Gateway

You click 'Pay' on a checkout page, the page hangs, and you panic for a second: did it go through, and if I click again, will I be charged twice? A payment system's entire reason for existing is to make sure the answer to that second question is always no. Of all the systems people study, this is the one where being merely 'mostly correct' is unacceptable, because every mistake is somebody's real money.

Here is the whole thing in plain terms. After a customer places an order, the e-commerce backend sends a payment event to the Payment Service. The payment service runs a risk check, then breaks the event into one or more payment orders and hands each to the Payment Executor. The executor talks to a Payment Service Provider (PSP) such as Stripe or Adyen, and the PSP is the one that actually moves money by talking to the card schemes, Visa and Mastercard. Most companies do not connect to the card schemes directly. They let a PSP handle the cardholder data and the rails.

When a payment succeeds, two internal books are updated. The Wallet records how much each seller is owed, and the Ledger records every movement using double-entry bookkeeping, so debits always equal credits. If those two sides ever disagree, that is a bug, not a missing dollar.

The idempotency key is the idea worth getting straight, and a coat check makes it concrete. When you hand over your coat you get a numbered ticket. Show that same ticket twice and you get the same coat back, not a second one. Exactly-once is just two simpler guarantees stacked together: at-least-once, because the executor retries failed calls, and at-most-once, because the idempotency key (passed all the way to the PSP) makes a retry return the original result instead of charging again. Retries that keep failing land in a dead letter queue for a human to look at.

The last line of defense is reconciliation. Every night the PSP and the banks send a settlement file listing what actually cleared. A reconciliation job compares that file against the ledger and flags any mismatch, because asynchronous systems drift and you cannot assume your own records are right. This system teaches the payment-service-and-executor split, why a PSP sits between you and the card schemes, double-entry ledgers and wallets, idempotency for exactly-once side effects, and reconciliation as the safety net under all of it.