Why a Balance Is More Than a Number: How Financial Systems Track Money

A customer has $100 in a wallet and makes a $30 purchase. The app now shows $70 available. The merchant has not received the money yet. The bank statement may not show the payment until later. These numbers can all be correct. They describe different stages of the same operation. To understand a financial system, it helps to separate three things: the instruction to make a payment, the records of what each party is owed and the evidence of what happened outside the system. Financial ledger architecture is largely about keeping those three things connected without treating them as interchangeable.Open the architecture diagram at full size

Start with the balance on the screen

In this example the wallet is prepaid: there is no credit facility, no overdraft and no other restriction on the funds. Before the purchase, its posted balance is $100. “Posted” means the amount is reflected in completed accounting entries. When the customer starts the purchase, the system reserves $30. The money has not disappeared from the posted balance, but it is no longer available for another purchase.
  • Before the purchase: $100 posted, $0 held, $100 available
  • While $30 is reserved: $100 posted, $30 held, $70 available
  • After the purchase is posted: $70 posted, $0 held, $70 available
If the purchase is cancelled before it is posted, the reservation can be released. The customer returns to $100 available without a posted debit followed by a refund. This is one model, not a universal balance formula. Incoming funds, credit limits, fees and risk restrictions can change what a product considers available. The names used by providers also differ. Define the meaning of each balance before exposing it in an API. Modern Treasury’s ledger account documentation illustrates why posted, pending and available balances need separate definitions.

What a ledger records

A ledger is a structured financial record. It records amounts against accounts, the entries that changed those amounts and the operations those entries belong to. An account here does not necessarily mean a bank account. It can represent a customer’s wallet entitlement, money owed to a merchant, a fee or an amount awaiting settlement. Consider a simplified marketplace. Posting a $30 purchase can reduce the platform’s liability to the customer by $30 and increase its liability to the merchant by $30. The platform owes the same total amount, but now it owes part of it to a different party. No bank transfer is implied by that internal entry. In double-entry accounting those changes are recorded as balanced debit and credit entries. Debit does not universally mean “money leaving”, and credit does not universally mean “money arriving”: the effect depends on the account type. The useful engineering property is that a transaction has matching sides, recorded together. TigerBeetle’s financial accounting guide explains these account types and balancing rules. A stored balance can still be useful for fast reads. What should not happen is an unexplained change from 100 to 70 with no durable record of which operation caused it. Store each amount together with its currency, using integer units with a defined scale or an exact decimal representation. Not every currency uses cents. Fees and conversions also need explicit rounding rules. See Stripe’s currency units and PostgreSQL’s exact numeric types for practical examples.

How the components fit together

The diagram separates six responsibilities. They can live in a small application or in several services. Six boxes do not require six separate deployments.
  • Application: accepts the customer’s instruction and displays the result
  • Payment service: tracks the operation, talks to the provider and requests the appropriate ledger changes
  • Payment provider: processes the external payment and reports its status
  • Ledger: records balanced entries and manages reservations under the product’s rules
  • Balance view: exposes understandable balances to the application. It can be a direct ledger query or a derived read model
  • Reconciliation: compares ledger records with bank or provider reports and surfaces differences for investigation.
This is a reference architecture, not a claimed client deployment. Arrows show requests and records, not literal movement of money. Ledger updates and provider actions have separate transaction boundaries.That last point matters. Successfully recording a purchase in the ledger does not prove that the merchant’s bank has received funds. Equally, an API response confirming that a provider accepted a request may say nothing about final settlement. The product must define what justifies each accounting transition: an authorised internal action for some, provider or bank confirmation for others.

Two purchases can spend the same money

Now imagine two $80 requests arriving while the customer has $100 available. Both requests read 100. Both decide there is enough money. If the check and reservation are separate, the system can accept $160 of spending. The fix is not a faster refresh on the balance screen. The authoritative system must check the available funds and reserve them as one atomic operation: either both happen, or neither happens. The second request must be rejected or rechecked against the reduced available balance. Depending on the implementation, this can use transactional database locking, version checks or ledger-native balance conditions. Modern Treasury documents balance and version locks for this purpose. A cached balance shown to the user should not become permission to spend an amount that has not been checked authoritatively.

A timeout is not proof that a payment failed

The payment service sends a request. The provider accepts it. The response is lost. From the application’s perspective, the request timed out. From the provider’s perspective, a payment may already exist. Creating a new payment immediately can charge the customer twice. Give the business operation a stable identity. Retries of that operation should resolve to the same intended effect, not create another one. This is idempotency. Apply it to internal accounting commands as well as provider requests; a provider’s protection does not stop your own code from writing the same ledger entry twice. Keep distinct steps distinct: reserving funds, submitting the payment, posting it and issuing a refund each need their own stable command identity. They are not retries of one interchangeable request. Provider guarantees have boundaries. For example Stripe’s idempotency documentation describes key retention and request-matching rules. Do not assume an old key protects an operation forever. Keep your own durable link between the business operation, provider objects and ledger transactions. When the outcome is uncertain, retain that uncertainty and recover it through the provider’s documented lookup or retry mechanism. Do not release a reservation merely because a network call timed out.

Notifications are evidence, not new payment instructions

A webhook is an HTTP notification sent by a provider when something happens. It can arrive twice or arrive after a later event. Stripe explicitly documents both behaviours in its webhook guidance. Our design recommendation is to verify the notification, save it durably, acknowledge delivery and then process it against the current operation state. A duplicate delivery must not create a second posting. An older notification must not blindly move a completed operation backwards. The same recovery question applies in the other direction: what if the service saves an operation and crashes before sending the provider request? A durable queue of work can preserve that intent. One common implementation is the transactional outbox: save the local state change and the work to send in one database transaction, then deliver it with retries. This does not put a remote ledger, a provider and your database into one transaction. Calls across those boundaries still need stable identifiers, recovery and explicit handling of partial completion.

Releasing a hold is not refunding a purchase

Before posting a hold can be released under the payment’s cancellation or expiry rules. After posting, changing the original financial entry would erase part of the history. Instead, record a new, linked transaction for the refund or correction. If the $30 purchase is followed by a recognised $10 refund, the example wallet ends at $80 posted and $80 available, assuming no other holds. The original purchase remains visible beside the refund.TigerBeetle’s pending-transfer model distinguishes posting, voiding and expiry. Modern Treasury’s transaction lifecycle similarly preserves posted financial entries and represents reversal through a separate transaction. These are concrete implementations of the distinction, not identical APIs. Whether external funds must arrive before crediting a refund is a product and accounting decision. The system should encode that decision rather than equate “refund requested” with “refund received”.

Why reconciliation is still necessary

A ledger can balance perfectly and still be wrong. A duplicated transaction can have balanced entries. A missing transaction can leave all existing entries balanced too. Reconciliation compares internal records with independent bank or provider evidence. Match operation identifiers, amounts, currencies and reporting periods. Account for known settlement delays, fees, refunds and adjustments. Comparing only two grand totals can hide errors that cancel each other out. When something does not match, create an exception with the records needed to investigate it. Do not silently edit a balance until it agrees with a statement. A necessary correction should be authorised, recorded and linked to its reason. Modern Treasury’s reconciliation documentation describes comparing ledger balances and external account balances.

What to test before calling the flow complete

A successful payment is only the first test. Also check two concurrent purchases against the same funds, a duplicated notification, a late status update, a timeout after provider acceptance, a partial refund and a statement containing a fee absent from the original request. For each case, check the accounting entries, the available amount and the recovery path, not only the status shown in the UI. The practical target is straightforward: when a customer asks why their balance changed, support should be able to follow the operation from request to reservation, posting and external evidence. Nobody should need to fix the explanation by editing the balance itself.
Why a Balance Is More Than a Number: How Financial Systems Track Money