Atomic Money: Making a PHP/MySQL Wallet Safe Under Concurrency
The lost-update bug that quietly corrupts homegrown wallet balances — and the five disciplines we used across PayWithToken to make money movement correct under concurrency. There is a bug that lives in a large share of the world's homegrown wallet systems. It doesn't throw an error. It doesn't show up in tests. It surfaces months later as a balance that is quietly, inexplicably wrong — and in a…
Here is the story in my own words:
Homegrown wallet systems often contain a bug that corrupts balances without any error or test indication. This "lost update" bug only manifests under concurrent traffic, potentially leading to customers losing money or companies giving it away. The bug occurs when a balance is read, modified in PHP, and written back, but two simultaneous requests can interfere with each other, causing the intended balance change to be lost.
To prevent this bug, PayWithToken implemented five key disciplines:
1. Let the database handle arithmetic: Instead of performing balance updates in PHP, update the balance directly in the database using an atomic statement. This ensures that concurrent updates are queued and executed in order, preventing lost updates.
2. Store money as DECIMAL, not FLOAT: Use the DECIMAL data type instead of FLOAT to avoid rounding errors and maintain exact monetary values. DECIMAL stores base-10 numbers with two fractional digits, providing precise monetary calculations.
3. Guard debits to prevent overdrawing: When deducting money from a user's balance, include a check within the same atomic statement to ensure the balance is sufficient. If the balance is insufficient, the debit operation is rolled back, preventing overdrawing the account.
4. Credit money at the time of confirmation, keyed to a specific payment: To avoid double-crediting a user for the same payment, tie the credit operation to a unique payment identifier, such as a bank reference or transaction ID. Use an INSERT IGNORE statement to ensure that if the payment has already been credited, subsequent attempts will be ignored, making the credit operation idempotent.
5. Make money movements transactional: When performing multiple money movements, such as debiting one wallet and crediting another, wrap the entire process in a database transaction. This ensures that either all the operations succeed, committing the changes, or none of them occur, rolling back the changes if an error occurs. This transactional approach maintains the integrity of the wallet system, preventing partial or inconsistent updates.
By applying these disciplined approaches, PayWithToken ensured that their wallet system remains accurate and reliable under concurrent traffic, preventing balance corruption and providing a secure payment experience for their users.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.

