How to Scale a Ledger, Part II: Mapping Financial Events
In this second part of the series, we'll look at what it means for a ledger to be immutable and double-entry at the source of financial events.

Updated on August 22, 2025.
This post is the second chapter of a broader technical paper, How to Scale a Ledger. In Part I, we covered why you should use a ledger database. In this chapter, we’ll cover core objects of a ledger, how some common financial events map into them, and how they are implemented in the Ledgers API.
This section focuses on the core fields and won’t be exhaustive of every feature available in the Ledgers API. See the documentation for a complete reference.
Adding Double-Entry to Financial Events
What does it take for a ledger to be immutable and double-entry at the source of financial events? At the most basic level, it means:
- Every monetary event is logged using consistent, double-entry data model.
- All balances shown to users and systems are read from this data model.
All financial flows—from credit card authorizations to crypto on-ramps—can be modeled with three core objects: account, entry, and transaction.
Account: The Sum of All Balances
An Account represents a discrete pool of value, and tracks a sum of money, denominated in a currency. Examples of Accounts include a digital wallet, a company’s operating cash, and loan principal.
Money moves between Accounts instantly within a ledger, even when real-world money movement happens asynchronously. For example, money sent via ACH takes at least a day to show up in a recipient’s bank account. Because real money movement can’t happen instantly, Accounts should be able to report a few different balances:
- Posted balance: fully settled funds
- Pending balance: fully settled funds plus posted funds (the money that’s expected to settle)
- Available balance: funds available to send out (does not include expected outgoing funds nor incoming unsettled funds)
Not all ledgers model posted, pending, and available balances; many require developers to model these balances separately as separate Accounts. We believe that application ledgers should have these core primitives, as they reflect fundamental concepts in payment flows and must be easily accessible and queryable.
Example: Credit Card Lifecycle
Let’s follow an example of a credit card to see how these balances are affected by different actions in the Ledgers API.
We report these three balances separately so that the customer knows how much can be spent from an Account at any given time. Depending on the use case and risk tolerance, an app may choose to use each balance for a different purpose. For example, a customer may be limited by their available balance for transfers out of their Account, but their past-due status for a loan may be optimistically determined by their pending balance.
Entry: An Immutable Record of Movement
Account balances are never directly modified. Instead, changes in balances are recorded as entries written to the Account. Entries are a complete log of balance changes into and out of an account, and include:
- Immutable core fields (
amount
,direction
, etc.) - A mutable
discarded_at
field (used only for pending reversals, and set when an Entry is replaced by a later Entry)
Let’s follow the same credit card example to see how each action can be represented as an Entry.
1. A credit card starts with a $10,000 credit limit on the account.
2. A card is swiped to purchase a $1,000 plane ticket. This purchase starts out pending on the card account.
3. That night, the purchase settles.
4. The card holder initiates a $1,000 card payment from their bank account.
5. The card payment from the bank account completes.
6. A hotel places a $250 hold on the card at the beginning of the stay, for incidentals.
7. The $250 hold is removed at the end of the stay.
Discarding Entries
Only pending Entries can be replaced; posted and archived Entries are permanent. Pending Entries get replaced in the following two circumstances:
- The pending amount of an Entry needs to change.
- The state of the Entry needs to change.
Why introduce this mutability in the API? To see why, it helps to consider the alternative. Instead of discarding Entries, we could create a reversal Entry that undoes the original Entry. In this model, moving an Entry from pending to posted would create two Entries instead of just one.
In that case, step five above would instead be:
While valid, this approach leads to a messy Account history. We can’t distinguish between debits that were client-initiated and debits that are generated by the ledger as reversals. Listing Entries on an Account doesn’t match our intuition for what Entries comprise the current balance of the Account.
Discarding provides a clean, audit-safe method of updating pending movements without compromising history. You can see the current Entries by excluding any Entries that have discarded_at
set.
Computing Account Balances
Now that all balance changes are logged as Entries, how do we compute Account balances? Here’s where the normal_balance
field on Account comes into play. This property defines how balances should respond to debit and credit entries.
Every Account in a ledger is categorized as debit normal or credit normal:
- Debit-Normal Accounts represent uses of funds (assets, expenses)
- Debit Entries increase the balance; credit Entries decrease it
- Credit-Normal Accounts represent sources of funds (liabilities, equity, revenue)
- Credit Entries increase the balance; debit Entries decrease it
Why bother with debits, credits, and Account normality at all? Many ledgers try to avoid complications by using negative numbers to represent debits and positive numbers to represent credits. At first glance, this appears to align better with engineers’ intuitions. But we chose to include Account normality, because without it, double-entry accounting is messy.
Consider a simple flow where a user deposits money into a digital wallet. This flow will affect the company’s cash Account and the user’s wallet Account. Our intuition is that the cash Account will increase (the company got cash from the user), and also the wallet Account will increase (the user has a positive balance in the digital wallet).
With a positive/negative number approach, it is impossible for both Accounts to increase. We have to pick one Account to be negative (so that no money is created or destroyed), and it’s not clear to which we should apply negative numbers.
Credits and debits solve this problem. We should debit the cash Account, and its balance increases because it is debit normal. And we should credit the user's wallet Account, and its balance also increases because it is credit normal. This mirrors real-world accounting logic and supports scale.
This digital wallet scenario is just one example. For a full primer on double-entry accounting, check out our series on Accounting for Developers.
Core Fields for Balance Calculation
Now, we’ll focus on how to actually implement a double-entry ledger. With some simple math, each type of balance (pending, posted, and available) can be calculated from the following five fields:
posted_debits
: sum of posted debit Entriesposted_credits
: sum of posted credit Entriespending_debits
:posted_debits
plus the sum of non-discarded pending debit Entriespending_credits
:posted_credits
plus the sum of non-discarded pending credit Entriesnormal_balance
: One of credit or debit, stored on the Account
A ledger database should be optimized to retrieve these five fields quickly, and only compute posted balance, pending balance, and available balance upon request.
Each balance type is then computed as follows:
Posted Balance
Pending Balance
Available Balance
Next Steps
This is the second chapter of a broader technical paper with a more comprehensive overview of the importance of solid ledgering and the amount of investment it takes to get right.
If you want to learn more, download the paper, or get in touch.
Read the rest of the series:
