Five ways a payments product loses money without crashing
Published by PWTECH
A payments feature can be demonstrated end to end, signed off and launched, and still be leaking money. None of the faults below produce an error page. They produce a number that is slightly wrong, discovered weeks later by an accountant or a customer.
This note lists the five we meet most often, what causes each one and the design that prevents it. The fixes are not exotic; they are habits that have to be in place before the first real transaction.
Five faults and the fix for each
A charge made twice
The customer taps Pay, the connection stalls, the app retries, and the processor sees two requests. The fix is an idempotency key generated when the payment is first attempted and sent with every retry, so the processor returns the original result instead of charging again.
A balance kept as one number
Two requests read the same balance, each subtracts its amount and writes the result; one update vanishes. The fix is a ledger of entries that always sum to zero, with the balance calculated from them, and corrections made as new entries rather than edits.
A webhook taken at its word
An endpoint marks an order paid because a request said so. The fix is to verify the signature on every event, then fetch the payment from the provider by its id before changing anything, and to accept the same event twice without acting twice.
A transfer that stops halfway
Money leaves one wallet, the credit to the other fails, and nobody is told. The fix is an explicit state for each transfer, a job that moves it forward or backward, and an alert when a transfer has sat in one state for too long.
Reconciliation left for later
The internal ledger, the processor report and the bank statement are never compared until the figures disagree. The fix is a daily job that compares all three and opens a ticket for each mismatch with the evidence attached.
What keeps these out of a codebase
Money as integers
Amounts are stored in the smallest unit, as whole numbers, with the currency beside them. Floating-point arithmetic never touches a balance.
Tests for the ugly paths
The test suite covers the retry, the duplicate event and the timeout, not only the happy path. These are the cases a demo never shows.
A console for support staff
Operations staff can look up a payment, see every state it passed through and issue a refund within their limits, so nobody edits the database by hand.
Asked by founders
Does the payment provider handle all of this for us?
The provider handles its side: the card, the bank, the settlement. Your ledger, your states and your reconciliation are yours, and the provider cannot see when your records disagree with its own.
We already launched. Where do we start?
With reconciliation. A daily comparison tells you within days whether the other four faults are present, and which one is costing the most.
Is a double-entry ledger too heavy for a small product?
It is a few tables and a rule that entries balance. The cost is small; the cost of rebuilding balances from a history of overwrites is not.
Draw the money flow and send it
Who pays whom, through which provider, and what should happen when a step fails. That sketch is enough for a useful first conversation. We reply within one business day.
