Defining Double-Entry
Clone Formance Ledger on GitHub
Run it locally, post your first balanced transaction, and inspect how debit and credit entries keep every movement auditable.
Clone Formance Ledger on GitHub
Run it locally, post your first balanced transaction, and inspect how debit and credit entries keep every movement auditable.
A $12,000 discrepancy shows up in settlement. Three engineers spend two days tracing it through application code and cannot find the root cause. Meanwhile, the customer support queue fills with duplicate-charge complaints because a retried webhook posted the same deposit twice, and both copies balanced perfectly.
A double-entry ledger is the primitive that prevents these situations from escalating. Every transaction posts balanced debit and credit entries across at least two accounts, so total debits always equal total credits.
This article disentangles what "double-entry" means, builds a formal model from first principles, and shows what it takes to implement one as a production programmable ledger. It builds on the concept of a ledger we discussed previously.
A double-entry ledger is a record-keeping system in which every transaction posts balanced debit and credit entries across at least two accounts, so the sum of all debits always equals the sum of all credits.
The balancing rule ensures that the accounting equation (Assets = Liabilities + Equity) remains true after every posting.
The established accounting terms are "double-entry bookkeeping" and "double-entry accounting," which name the same concept; "double ledger" and "double-ledger accounting" are nonstandard.
Four ideas get routinely conflated with double-entry: immutability, data duplication, debit/credit direction, and accounting standards. Mixing them up leads to ledgers that either over-promise (a mutable "double-entry" system) or under-deliver (an immutable log that cannot produce a balance sheet).
Double-entry and immutability solve different problems. Double-entry is an accounting principle that applies to every transaction's balance.
Immutability is an audit architecture in which history cannot be rewritten, and any correction must be appended rather than edited in place. You can technically build a mutable double-entry ledger (a bad idea, but a possible one), and you can build an immutable single-entry ledger.
If you are building a ledger for production, you want both properties. In an append-only ledger, corrections are made with a reversal transaction to back out the mistake, followed by a new transaction that records the correct movement.
Time requires the same discipline. Tracking bi-temporality (both when an event was recorded and when it was effective) is essential for late-settlement files and backdated corrections.
Formance Ledger implements both properties by linking each transaction's hash to the previous one, making tampering evident and keeping recorded and effective timestamps separate, so you can show an auditor exactly what the ledger knew at any date and when it knew it.
Double-entry accounting does not require writing every movement twice in a database. The number of database writes is an implementation detail.
Historically, when ledgers were handwritten into books, double-entry accounting involved recording corresponding debit and credit entries across separate books, which also helped surface accounting errors.
You can implement double-entry ledgers without literally making two writes through automated validation that reduces reliance on manually spotting typos.
Whether a debit or a credit increases an account's balance depends entirely on the account's type. Assets and expenses are debit-normal (debits increase them); liabilities, equity, and revenue or income are credit-normal (credits increase them). The directional accounting rules for debits and credits fall out of these two categories.
GAAP (Generally Accepted Accounting Principles) and IFRS (International Financial Reporting Standards) are accounting standards that sit atop a double-entry ledger.
They guide accountants in describing a business's financial position in a way other accountants can understand, and define business rules beyond the purely mathematical structure of a ledger, much like coding standards and linters for programming languages.
A double-entry model is built from three primitives: accounts, transactions, and entries.
| Primitive | Role | Key properties |
| Account | Container that value moves between | Has an identifier that can hold postings in multiple assets or currencies |
| Transaction | Atomic container for entries | Carries a description, posting time, and arbitrary contextual data. It may have any number of legs as long as debits and credits balance |
| Entry | Individual value movement inside a transaction | References its transaction and target account that carries an amount and a direction (debit or credit) |
In a double-entry model, each account has a position in the chart of accounts and a normal side, such as debit or credit, depending on whether debits or credits increase its value. An account's normal direction is determined entirely by its type.
Accounts have a balance equation:
The standard account categories:
These rules keep the accounting equation, Assets = Liabilities + Equity, true after every transaction. The extended form, Assets + Expenses = Liabilities + Equity + Income, explains why assets and expenses share a normal balance: both sit on the left side of the equation, so both grow with debits.
You will often see accounts drawn as T-account diagrams with a "T" shape with debits on the left and credits on the right, regardless of the account's type. The T-account is a teaching aid because the ledger itself remains the authoritative source for reports and audits.
A transaction is an atomic set of balanced entries. Each entry names an account, an amount, and a direction (debit or credit).
Each transaction must contain at least one debit and one credit, involve at least two accounts, and the sum of debits must equal the sum of credits. Some implementations relax the two-account requirement by allowing a transaction to debit and credit the same account.
The worked example below uses three accounts:
Suppose Alice wants to deposit $50,000 into her account, but the business charges a 10% fee. Expressed as balanced entries:
DEBIT banks:main 50000
CREDIT users:alice 45000
CREDIT platform:fees 5000
In Numscript, Formance's purpose-built language for describing financial transactions, the same movement is expressed by intent:
send [USD/2 5000000] (
source = @banks:main
destination = {
90% to @users:alice
10% to @platform:fees
}
)
Numscript describes what the transaction is meant to do. The ledger derives the balanced debit and credit entries and enforces the balancing constraint at commit time.
If the accounts were empty before this transaction, applying the balance equations yields balances of 50,000 for banks:main, 45,000 for users:alice, and 5,000 for platform:fees.
Across all accounts, the total debits (50,000) equal the total credits (45,000 + 5,000). No value appeared from thin air.
An entry is the atomic unit of value movement inside a transaction. Each entry references exactly one account and carries three pieces of information. It includes an amount, an asset (the currency or unit being moved), and a direction (debit or credit). Entries exist only as legs of a transaction, and a transaction is valid only if its entries balance.
In the Alice deposit above, three entries make up the transaction:
Each entry independently updates its target account's balance according to that account's normality: the debit increases banks:main (debit-normal), and the two credits increase users:alice and platform:fees (both credit-normal). The transaction is the atomic boundary; the entries are what actually move value.
Entries are also the grain at which audits and reports operate. Every figure in a balance sheet, trial balance, or account statement traces back to a specific set of entries, each tagged with its transaction, account, amount, asset, and direction. If the entries are wrong, every report built on top of them is wrong. But if the entries are correct and balanced, every report is reproducible.
Any platform that moves money on behalf of multiple parties needs a double-entry ledger. Single-entry cannot answer the questions that a regulated entity or a multi-party marketplace must answer: whose money is this, what claim does it represent, and do our internal books agree?
Three signals mean a double-entry ledger is no longer optional:
Omnibus accounts, FBO accounts, wallet balances, and merchant payouts all require per-entity balances that reconcile to a pooled bank balance. Only double-entry can produce both views from the same source of truth.
Reconciling ACH, card, wire, and crypto flows against internal balances requires an account structure that separates rail-of-origin from user-facing balances. Single-entry collapses that structure.
GAAP and IFRS reporting, DORA and MiCA compliance, and standard audit procedures assume double-entry books. Retrofitting the account structure after the fact is expensive.
If any one of them applies, you need to implement a double-entry ledger.
A production double-entry ledger has to enforce properties on every write path: bi-temporality, hash-chaining, per-entity account isolation, transactional invariants at commit time, and idempotent handling of retries and concurrent writes.
A single created_at column is not enough. Production ledgers need to track two independent time dimensions: when an event was recorded, and when it was effective. Late settlement files, backdated corrections, and point-in-time audit reports all require both.
Collapse the two into a single timestamp, and you lose the ability to answer "what did the ledger know on date X, and when did it know it?", which is the question every auditor eventually asks.
The log must be append-only and hash-chained, with each transaction referencing the hash of the previous one. Without this, there is no cryptographic evidence that the history the ledger returns today is the same history it returned yesterday.
Accounts must be isolated per entity, so a bug in one flow cannot corrupt another. A flat accounts namespace shares fate across every flow that writes to it.
One buggy service, migration, or manual fix can silently move balances that belong to unrelated tenants, ledgers, or products. Isolation belongs in the storage model, not in a WHERE clause the caller might forget.
All entries in a transaction must commit or fail together. A partially applied transaction is worse than a rejected one because it silently corrupts balances.
The double-entry constraints (at least one debit and one credit, and equal aggregate amounts per transaction) should be enforced at transaction commit in the storage layer, with API validation as a second safeguard.
A new service, a migration script, or a manual fix can bypass application convention. However, a storage-layer constraint applies to every write path, regardless of who writes.
A ledger without idempotency keys will post the same movement twice under retry, and both copies will balance perfectly. Design for retried payment webhooks, network timeouts, and replayed messages: require a retry-safe transaction identity and check an idempotency key at write time.
Concurrency requires equal care: serialize or lock conflicting postings so two processes cannot act on the same available balance and drive an account negative or double-spend funds. The hardest failures in an in-house implementation arise precisely at these boundaries, and most initial prototypes do not surface this complexity until production.
A production double-entry ledger must answer yes to seven questions. If it can't, the failure modes are silent, and the audit trail you need to prove nothing broke is the one you don't have. Verify each before your next design review: