Skip to content
All case studies

Why Balances Lie: Building Financial Systems on Ledgers, Not Numbers

Role: Software EngineerJuly 14, 20267 min read

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.

System DesignLedgersPostgreSQLPayments
Why Balances Lie: Building Financial Systems on Ledgers, Not Numbers

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?

Storing only a current balance leaves no record of what actually happened

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
balance
UPDATE 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   Coffee

The balance is just the running total.

500
-50
-30
-20
-----
400

At 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.

A ledger is a timeline of entries you can sum at any point in time

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     +100

One 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.

Double-entry: every transfer mirrors two entries that net to zero

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 -> Settlement

Which is why ledger entries often carry a state alongside them.

Pending -> Captured -> Settled

Payment state machine: 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.

A ledger turns "where did the money go?" into a query

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?