Testing money and transaction flows: double-charge, retries, reconciliation
This is the most important lesson in the mobile module, because it is where a bug does the most damage: money. When an app moves money — a top-up, a payment, a transfer, a withdrawal — a bug does not annoy a user, it takes or loses their money, and it does so at scale and under scrutiny. Testing transaction flows is the highest-stakes work a QA on such a product does, and it demands a specific, paranoid discipline. This lesson is how to test money flows, and the failure modes that matter most.
Why money flows are different
A cosmetic bug is embarrassing; a money bug is a loss. The stakes change how you test:
- The impact is real and irreversible-ish. Money moved wrong must be found, refunded, reconciled — and trust, once lost ("this app charged me twice"), is hard to win back.
- It scales silently. A transaction bug hits every transaction of that kind until noticed — thousands of users before anyone raises it.
- It attracts scrutiny. Regulators, auditors, and angry customers care about money in a way they never care about a layout glitch.
So money flows get tested hardest and most paranoidly. Every money-touching test asks not just "did it succeed?" but "did exactly the right amount move, exactly once, and can I prove the books balance?". The severity of any transaction-correctness bug is critical (the severity lesson), however small it looks in the UI.
The failure modes that matter
The specific ways money flows break, each a deliberate test:
- Double-charge / duplicate transaction. The single most important money bug. The user (or a retry, or a network glitch) causes the transaction to happen twice — charged twice, transferred twice. Causes: double-tapping a slow button (the connectivity lesson), a retry after a lost response, the app resubmitting on resume after an interruption. Test: double-tap confirm; drop the network after the request but before the response, then retry; background/kill mid-transaction and resume. Assert it happens exactly once.
- Lost transaction. The opposite: the money is taken/committed on the server but the app shows failure, or the transaction silently vanishes. The user is charged but got nothing, or paid and the recipient was not credited. Test: the lost-response case; failure exactly at commit.
- Wrong amount. Rounding errors (₹ with paise, floating-point mistakes), the wrong currency, a fee not applied or applied twice, off-by-one on the amount. Test: boundary amounts, amounts with decimals, the maximum, and verify the exact figure moved, to the paisa.
- Insufficient-funds and limits. Spending more than the balance/float, exceeding a daily/transaction limit. Test: exactly the balance, one over, well over, at and over each limit — and confirm it is correctly refused with no partial movement.
- Balance not reconciling. After a transaction (or several, or a failed one), does the displayed balance match the true server balance and the transaction history? A balance that drifts from the ledger is a serious bug. Test: do a sequence of transactions (including a failed/retried one) and verify balance = starting balance ± the transactions that actually happened.
- Negative or zero amounts. Can the user (or an attacker) enter a negative amount to gain money, or a zero to create junk transactions? Test: negative, zero, and — thinking like an attacker (the mindset lesson) — whether the server rejects them even if the app's field does not.
Idempotency and "exactly once"
The concept that underlies safe money handling, and that a QA on a money product must understand: idempotency — doing the same operation twice has the same effect as doing it once. A well-built payment API is idempotent: if the client sends the same top-up request twice (because of a retry or a double-tap), the server recognises it as the same transaction and processes it once, returning the same result — no double-charge. The mechanism is usually an idempotency key: the client attaches a unique key to the request, and the server, seeing a key it has already processed, returns the existing result instead of doing it again.
For a QA this is central: the whole point of the double-charge tests is to verify the system achieves exactly-once even when the client retries. So you deliberately cause retries (double-tap, drop-and-retry, resume-after-kill) and assert that the money moved once — which is the system's idempotency working. If it double-charges, idempotency is broken or missing, and that is a critical bug. You do not need to build idempotency, but you must test for its effect: "if I make this happen twice, does the money move once?".
Test the API directly, not just the app
Money bugs are often best found — and best proven — at the API/server level, not just through the app UI (the API-testing module):
- The UI might prevent a double-submit (a disabled button), but the server is what must be idempotent — so send the duplicate request directly to the API (bypassing the UI's guard) and check the server handles it. An attacker or a buggy client will hit the API directly; so must you.
- The server is where the money truly moves and the ledger lives, so verify at the source: after a UI action, check the API/database that the transaction record and balance are exactly right — the UI can show the right number while the backend is wrong (or vice versa).
- Negative/zero/oversized amounts must be rejected by the server, not only the app's input field — test the API with them directly.
So money-flow testing spans manual UI testing (double-tap, interrupt), connectivity testing (drop the network at the wrong moment), and API testing (send the duplicate/malicious request straight to the server and verify the ledger). This is exactly why the course covers all three — the worst money bugs sit at their intersection, and finding them takes all three skills.
The discipline: assume it can move money wrong
The mindset for money flows, stronger than for anything else: assume every transaction can go wrong — duplicate, vanish, be the wrong amount, leave the balance inconsistent — and try to make each happen, then prove it did not. For each money flow:
- Test the happy path and verify the exact amount moved once, and the balance/ledger reconcile.
- Force every duplicate path (double-tap, retry after lost response, resume after kill) and assert exactly-once.
- Force every failure path (network drop at each step, server error, timeout) and assert no partial or lost money.
- Test the amounts hard (boundaries, decimals, negative, zero, over-limit) — via the UI and the API.
- Reconcile: after a sequence including failures and retries, confirm balance = ledger = reality.
This is the highest-value testing on a money product, and it is where a QA earns their place several times over: catching one double-charge or reconciliation bug in testing saves a company-wide money incident. Test money flows like the money is real — because for the user, it is.
Check your work
Why money flows are different. A bug is a loss, not an annoyance — real, scaling silently across all transactions, and attracting scrutiny. Test them hardest and most paranoidly; any transaction-correctness bug is critical however small it looks.
The failure modes. Double-charge/duplicate (the top one — double-tap, retry-after-lost-response, resume-after-kill), lost transaction (charged but nothing / not credited), wrong amount (rounding, currency, fees, decimals), insufficient-funds/limits (refused with no partial move), balance not reconciling with the ledger, and negative/zero amounts (gaming). Test each deliberately.
Idempotency / exactly-once. A safe money system processes a repeated request once (via an idempotency key); the double-charge tests verify this exactly-once behaviour by forcing retries and asserting the money moved once. A double-charge means idempotency is broken — critical.
Test the API directly. The server must be idempotent and must reject bad amounts even when the UI does not — send duplicate/malicious requests straight to the API; verify the transaction record and balance at the source (the UI can lie). Money testing spans UI + connectivity + API.
The discipline. Assume every transaction can duplicate, vanish, be wrong, or leave the balance inconsistent — force each, prove it did not, and reconcile balance = ledger = reality. Test as if the money is real.
Practice
- For a top-up, test the double-charge paths: double-tap confirm; drop the network after the request before the response, then retry; kill the app mid-transaction and resume. Assert the money moved exactly once each time.
- Force a lost transaction (failure exactly at commit) and check the user is neither wrongly charged nor silently uncredited.
- Test amounts hard: decimals/paise, the maximum, one over a limit, zero, and negative — via the UI and by sending them straight to the API. Verify the exact figure and correct rejections.
- Do a sequence of transactions including one failed-and-retried, then reconcile: does the balance equal the ledger of transactions that actually happened?
- Explain idempotency and how you would test that a payment API achieves exactly-once under a duplicate request.
- Send a duplicate request directly to the API (bypassing the UI's disabled-button guard) and confirm the server, not just the UI, prevents the double-charge.
Official documentation
- Stripe — Idempotent requests — How payment APIs achieve exactly-once with idempotency keys (the concept to test for).
- OWASP — Testing for business logic (including transaction flaws) — Testing money/business-logic flaws, including the attacker view.
- Ministry of Testing — Testing payments and money — Practitioner writing on testing financial flows.
Next: HTTP for testers — requests, responses and status codes.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship