Why Balances Lie: Building Financial Systems on Ledgers, Not Numbers
Storing a current balance is the wrong source of truth. A practical walkthrough of how ledgers, double-entry, pending states, and idempotency keys fix the failures that appear the moment real money starts moving.
The Dispute Nobody Can Resolve
Imagine Alice sends Bob 100 dollars through an app.
Five minutes later, Alice calls support and says the payment never went through. Bob is already checking his account, and he swears he got the money. Support pulls up the database. There is a single number sitting in a balance column for each of them, and neither number tells anyone what actually happened five minutes ago.
Who is right?

If your system only stores a current balance, there is no way to answer that question with confidence. You can guess. You can check logs if you are lucky enough to have kept them. But you cannot point to a record and say "here is exactly what happened, in order, and here is the proof."
That gap is the entire reason ledger based systems exist, whether you are building a payment processor, a wallet, or plain old accounting software.
Why the Obvious Design Falls Apart
Most developers, myself included at some point, start with something like this.
Users
id
balanceUPDATE users
SET balance = balance - 100
WHERE id = 'alice'It looks perfectly reasonable. It is fast, it is simple, and it works fine in a demo.
Then reality shows up.
The server crashes halfway through a transfer. A request times out on the client side, so the user hits retry, except the first request actually went through and now they have been charged twice. A customer asks for a refund and someone has to manually recalculate what the balance should be. An auditor asks you to prove that every transaction over the last year adds up correctly, and you realize you have no way to reconstruct history, only the final number you are currently sitting on.
None of these are edge cases. They are Tuesday.
Here is the reframe that fixes all of it at once: the balance was never the real data. It was always a side effect. The real data is the sequence of things that happened. A balance is just what you get when you add all of that up at a single point in time.
Once that clicks, a lot of design decisions that used to feel arbitrary start to make sense.
How Ledgers Actually Work
Instead of storing a single number, you store a list of entries.
+500 Salary
-50 Netflix
-30 Groceries
-20 CoffeeThe balance is just the running total.
500
-50
-30
-20
-----
400At any moment, you can recompute the balance by summing every entry up to that point. If someone disputes a transaction, you are not staring at a number trying to reverse engineer what happened. You are looking at a timeline.

This leads to a rule that feels strange the first time you hear it, but is non negotiable once you understand why it matters: you never edit or delete an entry. If a transaction was wrong, you do not go back and change the old row. You add a new entry that corrects it. A refund is not an edit, it is a new transaction that cancels out the old one. A reversal is not a deletion, it is a new event that says "undo this."
This is not a clever engineering trick either. It is literally how bookkeeping has worked for centuries, long before computers existed. Accountants have always known that you do not erase history, you append to it. Software just borrowed the idea.
Double Entry, and Why It Shows Up Everywhere
Here is the part that trips up a lot of developers who have heard the phrase "double entry bookkeeping" without ever really understanding it.
The rule is simple: money never appears out of nowhere, and it never disappears either. It only moves from one place to another. So every transfer creates two entries that mirror each other.
Alice -100
Bob +100One side goes down, the other side goes up, by the exact same amount. If you add up every entry across your entire system, the total should always net out to zero. If it does not, something is broken, and you will know immediately instead of finding out three months later during an audit.

What makes this pattern worth learning properly is that it is not specific to payment apps at all. Accounting software uses the exact same idea with debits and credits in a general ledger. Wallet apps use it to track internal balances between users. Even a bank's core ledger, the actual system of record behind your checking account, works this way under the hood. Once you have seen it in one place, you start noticing it everywhere.
Where Payments Add Extra Complexity
Everything above holds for accounting software, wallets, and internal ledgers of all kinds. Payment systems bring in two extra wrinkles because money does not move instantly, and networks are not reliable.
Money moves in stages, not in one jump. When someone pays with a card, there is usually an authorization first, then a capture, then settlement, sometimes hours or days apart. Your ledger needs to reflect that reality instead of pretending the money landed the moment the button was clicked.
Authorization -> Capture -> SettlementWhich is why ledger entries often carry a state alongside them.
Pending -> Captured -> Settled
This is also why your balance sometimes should not update immediately even though the payment "went through" from the user's point of view. The pending state exists on purpose.
Networks fail, and people retry things. A user taps pay, the request times out on their end, so they tap it again. Except the first request actually made it to your server and processed successfully. Without protection, you now have two transfers instead of one.
The fix is an idempotency key, which is really just a way of asking "have I already seen this exact request before?" If the answer is yes, you return the result you already computed instead of processing it again.
Request comes in with key: abc123
Already processed? Yes.
Return previous result. Do nothing else.This is a natural consequence of everything else you have built. Once your system is based on immutable, appendable events, you need a way to recognize a duplicate event before it gets appended twice.
Why All of This Is Worth It
Go back to the CEO asking a version of a question you will eventually get asked in some form: "Where did this missing 4.17 dollars go?"
With a ledger, this is a query. You walk the entries, you find the exact point where the numbers stop matching, and you can point to the specific event that caused it.
Without a ledger, this is guesswork, and usually a very stressful afternoon.

Here is a short list of the mistakes that tend to sneak in when teams skip this approach, usually because a balance-only design felt "good enough" early on.
- Updating balances directly instead of deriving them from entries
- Deleting or editing transactions instead of appending corrections
- Using floating point numbers to represent money, which introduces rounding errors
- Skipping idempotency keys, leading to duplicate charges on retries
- Missing transaction boundaries, so a crash leaves data in a half finished state
- No audit log, so nobody can answer "what happened and when"
- Treating the derived balance as if it were the source of truth
Every one of these is fixable early, and painful to fix later once real money and real customers are involved.
The Takeaway
We did not just design a wallet or a payment flow. We learned something that applies far beyond any single system.
A balance is not data. It is a projection of history, recalculated on demand. The ledger, the actual sequence of things that happened, is the real source of truth. Once you see that distinction clearly, a lot of decisions that used to feel like arbitrary engineering choices, immutability, double entry, idempotency, pending states, stop feeling arbitrary. They are just the natural consequences of taking that one idea seriously.
If you are building anything that touches money, whether it is a payment gateway, a wallet, or plain accounting software, start by asking yourself one question before you write a single table.
Is this a number, or is this history?