Building a wallet that cannot quietly lose money
By Mahadi Zulfiker, full-stack software engineer, Dhaka
The simplest wallet is a balance column and an UPDATE that adds or
subtracts from it. It works in a demo. In production, two requests that read
the same balance and both write back will lose one of the changes, and
afterwards nobody can say where the money went, because the only record is
the number that is now wrong.
Smart Sheba pays commissions, rank rewards, and profit shares out of every purchase, to many people at once. That is exactly the shape where a naive balance breaks. So the wallet is a ledger.
Entries, not balances
Every movement of money is a row that is never edited or deleted: a credit or a debit, an amount, a category such as a commission or a purchase, and the running balance after it. A balance is something you derive from entries, not something you overwrite.
Two consequences fall out of that:
- History is the source of truth. If a balance ever looks wrong, the entries that produced it are all still there.
- Fixes are new entries. A mistake is corrected with a reversing entry, so the correction is itself on the record.
Group everything one event caused
A single purchase can fan out into a dozen payouts across the referral and placement trees, plus a share to the rank pool and the admin fund. All of those entries carry the same transaction id. That one field is what makes the system auditable: pick any payout and you can see the purchase that caused it and every other entry that purchase produced, and check that they sum to what the purchase was worth.
Money is a decimal, never a float
Amounts are stored as fixed-precision decimals. Floating point cannot represent most decimal fractions exactly, and a wallet that drifts by a fraction of a paisa per transaction is a wallet that does not reconcile.
Prove it adds up
A ledger only helps if something checks it. Reconciliation jobs recompute balances from the entries and compare them with what the system believes, and a price-split integrity check confirms that for every sale the pieces paid out (commissions, rank pool, platform fund) add back up to the price. When the Sales Force rank system was rebuilt, these checks are what made the migration safe: the old and new payout rules could be compared entry by entry rather than trusted.
The tradeoff
An append-only ledger is more code than a balance column, and more careful queries: reading a balance has to be fast without giving up the history behind it, which is why each entry stores the running balance after it. It is also less forgiving of shortcuts, since you cannot just "fix the number". That is the point. For money, the version that is more work is the only version that stays correct.
What I'd tell someone starting
- Never store a wallet as a single mutable number.
- Give every entry a transaction id that ties it to its cause.
- Use decimal types for money, from the first migration.
- Write the reconciliation check before the first payout feature, not after the first support ticket.