Like a careful accountant: every rupee that moves is written twice (from where and to where) in a book that is never erased, every request carries a receipt number so it can't be paid twice, and at the end of the day the book is checked against the bank's statement.
Design the payment system of an e-commerce company: when a customer checks out, charge them through external payment providers (cards, UPI, wallets), record the money movement correctly, and later pay out to sellers. The bar is correctness, not throughput: no double charges, no lost payments, and books that always balance.
POST /v1/payments
Idempotency-Key: order-9812-attempt-1
{ "order_id": "9812", "amount": 49900, "currency": "INR", "method_token": "tok_..." }
-> 201 { payment_id, status: "PROCESSING" | "REQUIRES_ACTION", next_action_url? }
POST /v1/payments/{id}/refunds { amount } Idempotency-Key: ...
GET /v1/payments/{id} -> { status, amount, psp_reference }
POST /webhooks/psp (signed by the PSP; verified before use)
Amounts are integers in the smallest currency unit (paise, cents), never floating point.
checkout -> Payment service
1. idempotency check (key -> existing payment? return it)
2. create payment row: status=CREATED (same txn as key)
3. call PSP with OUR payment id as the PSP idempotency key
4. status=PROCESSING / REQUIRES_ACTION (redirect for 3DS/UPI)
PSP --> webhook --> verify signature --> update payment status (state machine)
--> write ledger entries (double entry)
--> publish PaymentSucceeded -> order service
Reconciliation job (daily): PSP settlement file vs our ledger -> mismatches -> alerts/queue
Status poller: payments stuck in PROCESSING too long -> query PSP for the truth
CREATED -> PROCESSING -> SUCCEEDED -> (PARTIALLY_)REFUNDED
| \-> REQUIRES_ACTION -> PROCESSING
\-> FAILED
UPDATE payments SET status = 'SUCCEEDED', psp_reference = :ref
WHERE id = :id AND status IN ('PROCESSING','REQUIRES_ACTION');
-- transitions are conditional: a duplicate or late webhook changes nothing
Idempotency-Key is stored with a unique constraint in the same transaction that creates the payment; a retry returns the stored result.ledger_entries(id, txn_id, account, direction, amount, currency, created_at) -- append-only
Customer pays 499.00, platform fee 50.00:
txn 77: DEBIT psp_clearing 49900
CREDIT seller_payable 44900
CREDIT platform_revenue 5000
invariant: sum(debits) = sum(credits) for every txn (checked in the same DB transaction)
Refunds are new reversing entries, never edits or deletes.
If the call to the PSP times out, we don't know whether the customer was charged. Never assume failure. Mark the payment PROCESSING, and let the webhook or a status poll (querying the PSP by our payment id) decide; the PSP-side idempotency key makes a retry safe.