Payment Systems: Ledgers, Double-Entry Accounting and Reconciliation
How payment systems stay correct: double-entry ledgers, payment state machines, idempotent PSP calls, webhooks and daily reconciliation, step by step.
A payment system must never lose money, create money, or charge a customer twice, even when networks time out and services crash halfway through. The designs that achieve this share a few ideas: a double-entry ledger as the source of truth for balances, an explicit payment state machine, idempotency keys on every external call, asynchronous webhooks from payment service providers (PSPs), and daily reconciliation that compares your records with the provider's and the bank's.
The flow of a card payment
A payment state machine
| State | Meaning | Next states |
|---|---|---|
| created | Intent recorded with amount, currency and idempotency key | authorising |
| authorising | Request sent to the PSP; outcome may be unknown | authorised, failed, unknown |
| authorised | Funds reserved on the card | captured, voided |
| captured | Funds taken; will settle to the merchant | refunded (partially or fully) |
| failed / voided | No money moved | terminal |
| unknown | Timed out; outcome must be queried, never assumed | resolved by status query or webhook |
Only allow valid transitions, store every transition with a timestamp and reason, and never move from unknown to failed just because a timeout happened. The charge may have succeeded. See partial failure.
Idempotency everywhere
- The client sends an idempotency key with the payment request; retries with the same key return the same payment instead of creating another. See idempotency.
- The payment service passes its own idempotency key to the PSP, so retrying a timed-out authorisation can't charge twice.
- Webhooks can arrive more than once and out of order; handlers deduplicate by event ID and apply state transitions only if valid from the current state.
Double-entry ledger
Instead of storing a balance and updating it, record every movement of money as a transaction with at least two entries that sum to zero: a debit from one account and a credit to another. Balances are derived by summing entries. The ledger is append-only: mistakes are corrected with new reversing entries, never by editing history.
| Transaction | Account | Debit | Credit |
|---|---|---|---|
| Customer pays ₹1,000 for an order | Customer funds receivable (PSP) | ₹1,000 | |
| Merchant payable | ₹970 | ||
| Platform fee revenue | ₹30 | ||
| Payout to merchant | Merchant payable | ₹970 | |
| Bank cash | ₹970 |
- Invariant: the entries of every transaction sum to zero, enforced by the database or the ledger service, so money can't appear or vanish.
- Amounts as integers in minor units (paise, cents) with a currency; never floating point.
- Auditability: every balance can be explained by its entries, which regulators and finance teams require.
- Strong consistency: ledger writes use transactions in a relational or distributed SQL database.
Reconciliation
Every day, compare internal records with the PSP's settlement reports and bank statements: each captured payment should appear in settlement with the expected amount and fees, and each payout should appear in the bank. Mismatches (missing, duplicated, wrong amount) go to an exceptions queue for investigation and correcting entries. Reconciliation catches what idempotency and state machines miss, such as a webhook that never arrived.
For events to other systems (emails, order fulfilment), publish them with a transactional outbox in the same database transaction as the state change, so a crash can't leave the payment captured but the order unfulfilled.
Where it’s used
Idempotent payment creation, a state machine for card and UPI flows, and webhook-driven confirmation.
Double-entry ledgers split each payment between seller payable, platform fees and taxes, then track payouts.
Balances derived from append-only ledger entries with daily reconciliation against bank statements.
In an interview
- Model payments as a state machine, including an unknown state after timeouts.
- Use idempotency keys from client to PSP and deduplicate webhooks.
- Store money movements in an append-only double-entry ledger with integer amounts.
- Reconcile daily with PSP and bank reports.
Further reading
- Avoiding Double Payments in a Distributed Payments SystemAirbnb Engineering
- Designing robust and predictable APIs with idempotencyStripe Blog