Foundational Requirements: Idempotent Operations & Double-Entry
Financial systems operate under zero-tolerance constraints for data loss or incorrect balances. A production payment system must guarantee:
- Idempotency: Retries of one intended payment use the same operation key and payload. Local uniqueness prevents duplicate local operations; preventing duplicate external charges additionally depends on the PSP's idempotency contract and reconciliation.
- Double-Entry Bookkeeping: Every financial transaction must record equal and offsetting debits and credits across balance sheet accounts. Money is never created or destroyed; money only moves between accounts.
- Auditability & Immutability: Ledger entries are append-only. Mistakes are corrected by recording reversing journal entries, never by mutating or deleting past records.
Payment Flow Architecture
Idempotency Execution Pattern
Clients generate a cryptographically unique Idempotency-Key (UUID v4) for every checkout attempt. When the request reaches the payment gateway:
- An atomic database transaction creates a payment record in
PENDINGstatus indexed by the idempotency key. If a row already exists, the gateway returns the existing status without charging again. - After committing the local operation, the service calls the PSP with the same stable key and frozen payload. It does not hold a database transaction open across the network call.
- If authorization is confirmed, a local transaction records
SUCCEEDEDand the corresponding ledger rows. If the call times out, retainUNKNOWN: the PSP may have charged the customer even though no reply arrived. Recover by repeating the identical provider operation where its contract permits, querying provider state, processing signed webhooks idempotently, and reconciling settlement data.
Nightly Reconciliation & Settlement
External PSPs and acquiring banks transmit end-of-day settlement batch files. The internal Reconciliation Engine matches internal ledger journal entries against the bank statement transactions. Any discrepancy (missed charge, chargeback, foreign exchange difference) triggers automated alerting and dispute queues for accounting review.
Worked example: ledger entries for one card payment
A merchant sells a 100.00 USD order. The PSP fee is 2.9% + 0.30, so 3.20 USD, and the PSP pays out the net amount. Amounts are stored in integer cents.
| Journal entry | Account | Debit | Credit |
|---|---|---|---|
| Capture | PSP receivable (asset) | 9680 | |
| Processing fees (expense) | 320 | ||
| Sales revenue | 10000 | ||
| Payout (T+2) | Bank cash | 9680 | |
| PSP receivable | 9680 | ||
| Refund of 20.00 | Sales returns (contra-revenue) | 2000 | |
| PSP receivable (netted from the next payout) | 2000 |
In every journal entry, debits equal credits. A nightly check that each entry balances, plus matching the PSP receivable balance against the payout and settlement files, catches most bugs. Whether the original fee is returned on a refund depends on the PSP's pricing, which is one more line the reconciliation must check.
Payment state machine
Card authorisations usually expire after about 7 days (this varies by network and merchant category), so a capture must happen before then or the hold on the customer's funds lapses. Only move between states along allowed transitions, using a conditional update, the same pattern as seat booking.
Reconciliation example
| Finding | Likely cause | Action |
|---|---|---|
| In the PSP file, missing from the ledger | Timeout left the payment UNKNOWN, and the webhook was lost | Create ledger entries from the PSP record, and mark the order paid |
| In the ledger, missing from the PSP file | Capture never actually happened, or will settle in the next batch | Recheck the next day, then investigate |
| Amount differs | FX conversion, fee change, partial capture | Post an adjustment entry |
| Chargeback line | Customer disputed the payment with their bank | Open a dispute case, and reverse the merchant payable |
Stripe keeps idempotency keys for at least 24 hours and returns the original response for any repeated request with the same key. Your own idempotency table should keep keys at least as long as the longest client retry window.