Double-Entry Accounting

The foundational discipline beneath every financial ledger. Covers the problem double-entry solves, the accounting equation as a conservation law, debits and credits as directions, the journal-ledger-trial-balance pipeline, and the software realization: derived balances, integer money, immutability, idempotency, failure modes, scaling, and alternatives.

Learning outcomes

Almost every financial system you will ever build or operate has a ledger at its center, and almost every ledger worth trusting is double-entry. Get this one idea right and a huge amount of fintech stops looking like a pile of special cases and starts looking like one principle applied over and over. Get it wrong and you will spend your career chasing pennies that do not add up.

After studying this page, you can:

  • Explain what problem double-entry accounting solves and why a single running balance is not enough for money you have to answer for.
  • State the accounting equation and treat it as a conservation law that every transaction must preserve.
  • Read and write debits and credits as directions of movement, not as good news and bad news, and say which side increases each kind of account.
  • Trace a real transaction from the event that caused it, through the journal and the ledger, to a trial balance that proves the books are internally consistent.
  • Design the core of a software ledger: why balances are derived and not stored, why money is an integer, why entries are immutable, and how idempotency stops a retry from creating money out of nothing.
  • Name the ways a double-entry system fails in production and the controls that catch each one before it reaches a customer or a regulator.

Before we dive in

You need no accounting background to start. We will use only a handful of words, and we will define each one the first time it appears.

An account is a named bucket that money or value sits in or flows through: a customer’s cash balance, a revenue account, a fee account, the float you hold at a bank. A transaction is a single economic event, for example a customer depositing one hundred dollars. A posting (also called an entry or a leg) is one recorded movement of value into or out of one account. The whole idea of double-entry is in its name: every transaction is recorded as at least two postings that move equal value in opposite directions, so the books always balance.

One more convention before we go further. Throughout this page, money amounts are shown in dollars for readability, but a real system never stores money as a decimal. We will see why in section six. For now, hold the picture of buckets and movements between them, and watch the single rule that never breaks: the value that leaves one place arrives somewhere else, and the two are always equal.

Mental Model

The wrong model, and almost everyone starts here, is that a debit is a decrease and a credit is an increase, or worse, that a debit is bad and a credit is good. Your bank reinforces this: when the bank “credits your account” you have more money, and a “debit card” takes money away. So the words feel like they mean minus and plus.

They do not. The reason the bank’s language feels backwards is that the bank is describing its own books, not yours. Your deposit is money the bank owes you, so on the bank’s books your balance is a liability, and liabilities go up on the credit side. The bank credits your account because, to the bank, your account is a credit account. The words were never about good and bad. They are about which side of an account a movement lands on.

Here is the model to hold instead. Picture every transaction as water moving through pipes between tanks. Water never appears or disappears; it only moves. Each movement has a source and a destination, and the amount that leaves the source exactly equals the amount that arrives at the destination. “Debit” and “credit” are just the names for the two ends of the pipe: the left end and the right end. A transaction is balanced when the total flowing out of the left ends equals the total flowing into the right ends. That conservation, value is moved and never minted by accident, is the whole point, and every rule below falls out of it.

Breaking it down

The core teaching runs in ten steps. The first four build the accounting machine the way a fifteenth-century merchant would have understood it. The next six rebuild it the way a modern fintech engineer must, because the principle is ancient but the failure modes are new.

1. The problem double-entry was invented to solve

Imagine you run a business with a single number for your cash: a running balance you adjust up and down. A customer pays you, you add to the balance. You buy supplies, you subtract. This is single-entry bookkeeping, and it is exactly how most people track a personal checkbook.

Single-entry has a fatal weakness for anything you must answer for: it records that the balance changed, but not why, not against what, and not whether the change was even legitimate. If the number is wrong, nothing tells you. If you subtract one hundred for supplies but type one thousand, the balance is simply wrong and stays wrong. There is no second record to disagree with the first. A single-entry system cannot catch its own errors, because there is nothing to cross-check against.

Double-entry solves this by recording every transaction in two places at once, as two equal and opposite movements. When the customer pays you one hundred dollars, you do not just increase cash. You record that cash went up by one hundred and that the source of that cash, say sales revenue, went up by one hundred at the same time. Now the books carry their own proof: at all times, the sum of everything must net to zero, because every movement was entered on both ends. If it does not net to zero, you know with certainty that something was mis-recorded, before you ever ship a statement to a customer or a filing to a regulator.

The same deposit, single-entry vs double-entry
Cash balance: +$100. That is the entire record. If someone later types +$1000 by mistake, or skims $100 to a hidden account, the books still 'look' fine. There is no second record to contradict the first, so the error is invisible until a human notices the money is gone.

This is the first thing to internalize: double-entry is not paperwork for its own sake. It is a self-checking structure. The redundancy is the feature. You pay for it by recording everything twice, and you get back a continuous, automatic integrity check on every dollar you touch.

2. The accounting equation is a conservation law

Every account in the world falls into one of five types, and those five types are tied together by a single equation that must always hold:

Assets = Liabilities + Equity

Read it as a statement about a business at one instant. Assets are what the business controls and expects value from: cash in the bank, securities it holds, amounts customers owe it. Liabilities are what the business owes to others: customer deposits, money borrowed, bills not yet paid. Equity is what is left over for the owners after liabilities are subtracted from assets, the residual claim. The equation says the value a business controls is always exactly matched by the claims against it, some claims from outsiders (liabilities) and the rest from owners (equity). There is no value with no claim on it, and no claim with no value behind it.

Two more types extend the picture over time. Revenue increases equity (the business earned something), and Expenses decrease equity (the business consumed something). So the full relationship is:

Assets = Liabilities + Equity + Revenue - Expenses

Now the key insight. Double-entry exists to keep this equation true after every single transaction. That is why entries come in balanced pairs. If a transaction increased the left side without an equal change that keeps the right side matched, the equation would break. By forcing equal debits and credits, double-entry makes it structurally impossible to record a transaction that violates the equation. The balancing rule is not a separate law you also have to remember. It is the same conservation law, applied one transaction at a time.

The five account types and which side increases them
What you control and expect value from: cash, securities, receivables. INCREASE with a debit, decrease with a credit. A customer's cash you hold is an asset to them but a liability to you.

The pattern across those five is worth memorizing because it never changes: assets and expenses increase on the debit side; liabilities, equity, and revenue increase on the credit side. Everything else in practical bookkeeping is an application of those two lines plus the balancing rule.

3. Debits and credits are directions not judgments

Now we can be precise about the two words that trip everyone up. A debit is a movement recorded on the left side of an account. A credit is a movement recorded on the right side. That is the entire definition. Whether a debit makes an account go up or down depends only on the account’s type, which you just saw in the tabs above.

This is why “debit equals decrease” is wrong. A debit increases an asset and decreases a liability. A credit decreases an asset and increases a liability. The same word points in opposite directions depending on the account, because debit and credit are about sides, not amounts. A useful way to hold it: a debit and a credit are the left foot and the right foot of every step the money takes. Neither foot is forward or backward on its own; it depends which way the body is walking.

The balancing rule, stated exactly, is this: in any transaction, the sum of the debits must equal the sum of the credits. A transaction can have many legs (a payment split across a fee, a tax, and a net amount, for example), but the debit total and the credit total must match to the cent. If they do not, the transaction is not allowed to post. This single rule is what a software ledger enforces on every write, and it is the reason a correct ledger can never silently leak or mint money.

Try it. Decide whether each movement below is a debit or a credit, then check your reasoning.

Check yourself
A customer deposits $100 into their account at your fintech app. From YOUR company's books, how is the customer's balance affected?

If that one felt backwards, that is the bank-language instinct fighting you. Re-read the Mental Model: the customer’s deposit is your liability, and liabilities rise on the credit side. Once that clicks, the rest of accounting stops feeling arbitrary.

4. From event to journal to ledger to trial balance

A transaction does not go straight into account balances. It flows through a small pipeline that has barely changed in five hundred years, because each stage does a job the next one depends on.

flowchart LR
  E["Economic event<br/>(customer pays $100)"] --> J["Journal<br/>(chronological list<br/>of balanced entries)"]
  J --> L["Ledger<br/>(entries sorted<br/>into accounts)"]
  L --> T["Trial balance<br/>(sum of all debits<br/>= sum of all credits)"]
  T --> F["Financial statements<br/>(balance sheet,<br/>income statement)"]

The journal is the chronological record: every transaction, in the order it happened, written as a balanced set of debits and credits. It is the source of truth, the diary of what occurred. The ledger is the same information reorganized by account: instead of “what happened on Tuesday,” it answers “everything that ever moved through the Cash account.” Posting is the act of copying each journal leg into its account. The trial balance is the integrity check: list every account’s net balance, sum all the debit balances and all the credit balances, and confirm the two totals are equal. If they are, the books are internally consistent. If they are not, a posting was dropped or duplicated, and you go find it.

Walk a single transaction through the whole pipeline.

A $100 deposit, end to end
EventA customer deposits $100. This is the real-world economic event. Nothing is recorded yet; we have only observed that something happened.
Step 1 of 5

Notice what the pipeline buys you. The journal preserves history in order, so you can always reconstruct how you got to today. The ledger answers per-account questions fast. The trial balance is a cheap, continuous proof that nothing was lost in translation between the two. Each stage exists because the next one needs what it produces, and the same shape survives in modern systems with the names changed: journal becomes an event log or a postings table, ledger becomes an account-balances view, trial balance becomes a continuous invariant check.

5. The ledger as the source of truth in software

Here is the single most important design decision in any financial system, and the place engineers most often go wrong: store the movements, derive the balances.

The naive design keeps a balance column on each account and updates it in place: read the balance, add the amount, write it back. It is tempting because it is simple and the balance is right there to read. It is also how you eventually lose money you cannot account for. An in-place balance has no history, so when it is wrong you cannot see how it got wrong. Two concurrent updates can read the same starting balance and both write back, so one update vanishes (a lost update, the classic race condition). And a balance that is the only record of the truth has nothing to check itself against, which throws away the entire benefit of double-entry.

The correct design stores an immutable list of postings and treats the balance as a derived quantity: the balance of an account is, by definition, the sum of its postings. You never overwrite a balance; you append a new posting, and the balance follows. This is the same insight as event sourcing: the log of what happened is the truth, and every current value is a fold over that log.

-- The truth: an append-only list of balanced postings.
CREATE TABLE postings (
  id            BIGINT PRIMARY KEY,
  transaction_id BIGINT NOT NULL,   -- groups the legs of one transaction
  account_id    BIGINT NOT NULL,
  direction     SMALLINT NOT NULL,  -- +1 debit, -1 credit
  amount        BIGINT NOT NULL,    -- minor units (cents), never a float
  currency      CHAR(3) NOT NULL,
  created_at    TIMESTAMPTZ NOT NULL
);

-- The balance is derived, never stored as the source of truth.
SELECT account_id,
       SUM(direction * amount) AS balance
FROM   postings
WHERE  account_id = $1
GROUP  BY account_id;

Two invariants live on top of this table, and a correct ledger enforces both on every write. First, per transaction, the signed amounts must sum to zero: debits equal credits, so a single transaction can never change the total money in the system. Second, across the whole ledger, the sum of all postings is always zero, which is the software form of the trial balance. You can run that second check continuously, and if it ever returns a non-zero number, a bug created or destroyed value and you want to know immediately.

The animation shows the whole shape of a balanced posting at once: an event arrives, splits into two equal legs, each leg posts to its account, and both roll up into a trial balance that nets to zero. Watch the conservation hold from the first frame to the last.

If you take one idea from this whole page into your next system design, take this one: the postings are the truth, the balance is a query. Everything good about a ledger, its auditability, its self-checking, its ability to be reconstructed after a disaster, comes from refusing to overwrite the truth in place.

6. Representing money without losing a cent

A ledger lives or dies on exact arithmetic, and the most common way engineers break a ledger on day one is by storing money as a floating-point number. Do not do this. A binary float cannot represent most decimal fractions exactly, so 0.1 + 0.2 is not 0.3, and across millions of postings those tiny errors accumulate into balances that will not reconcile and a trial balance that drifts off zero for no visible reason.

The standard fix is to store money as an integer count of the smallest unit of the currency, its minor unit. US dollars are stored as an integer number of cents, so one dollar is 100. Currencies differ in how many decimal places they use: the Japanese yen has zero (the minor unit is the yen itself), most currencies have two, and some, like the Bahraini dinar, have three. A correct system records the currency and its scale alongside every amount and never mixes currencies in a single balance.

// Money is an integer count of minor units, plus its currency. Never a float.
type Money struct {
    MinorUnits int64  // cents for USD: $1.00 is 100, $0.01 is 1
    Currency   string // ISO 4217 code, e.g. "USD", "JPY", "BHD"
}

// Addition is exact and only valid within one currency.
func (a Money) Add(b Money) (Money, error) {
    if a.Currency != b.Currency {
        return Money{}, fmt.Errorf("cannot add %s and %s", a.Currency, b.Currency)
    }
    return Money{MinorUnits: a.MinorUnits + b.MinorUnits, Currency: a.Currency}, nil
}

Integers solve exactness, but they do not solve division, and division is where real money quietly leaks. Split a $10.00 refund three ways and each share is 333 cents with one cent left over, because 1000 does not divide evenly by three. You cannot give each party 333.33 cents; cents are indivisible in the ledger. So a real system must decide, explicitly and consistently, where the leftover cent goes (often called the residual or the rounding remainder), and it must allocate the whole amount so the parts still sum to the original to the cent. The rule is absolute: an allocation must be exhaustive. If 1000 cents go in, exactly 1000 cents must come out across the shares, never 999 and never 1001. The largest-remainder method is one common, fair way to assign the leftover units deterministically.

This is the unglamorous heart of correctness in financial software. Doubles lose pennies to representation error; naive division loses pennies to rounding; and pennies that vanish make a double-entry system fail its own balance check. Use integers, carry the currency, and make every division account for its remainder.

7. Immutability idempotency and the append-only ledger

Two properties turn a postings table into a ledger you can actually trust in production: it is immutable, and its writes are idempotent.

Immutable means you never update or delete a posting once it is written. If a transaction was wrong, you do not erase it; you write a new, compensating transaction that reverses it (a debit where there was a credit, and the reverse), and then you write the corrected one. The original stays in the record forever. This is not bureaucratic caution. It is what makes the ledger auditable: the history is complete and tamper-evident, so anyone can reconstruct any past balance as of any moment by replaying the postings up to that time, and no one can quietly rewrite what happened. A corrected mistake leaves three records (the error, its reversal, the fix), and that trail is a feature, because it is the truth about what occurred.

stateDiagram-v2
  [*] --> Pending: transaction proposed
  Pending --> Posted: legs balance, write succeeds
  Pending --> Rejected: legs do not balance
  Posted --> Reversed: error found, write compensating entry
  Reversed --> [*]
  Posted --> [*]
  Rejected --> [*]
  note right of Posted
    Posted entries are never edited or deleted.
    A correction is a NEW reversing transaction.
  end note

Idempotent means that applying the same transaction twice has the same effect as applying it once. This matters because distributed systems retry. A network call times out, the client is not sure whether the payment went through, so it tries again. Without protection, the second attempt posts a second set of entries and the customer is charged twice, or paid twice. The fix is an idempotency key: the client attaches a unique key to the request, the ledger records which keys it has already applied, and a repeat of the same key returns the original result instead of posting again. The retry collapses onto the first attempt, and the duplicate vanishes.

A retried payment, with and without an idempotency key
The client sends 'transfer $100', the network drops the response, the client retries. The ledger now has TWO balanced transactions for one real payment: $200 moved, the customer is charged twice. Both transactions balance perfectly, so the trial balance is happy. Double-entry did not save you, because each duplicate was internally valid.

The Toggle hides a subtle and important lesson. Double-entry guarantees that each transaction is internally balanced, but it does not by itself guarantee that you posted the right number of transactions. Balancing and idempotency are different defenses against different failures: balancing stops a single malformed entry, idempotency stops a correct entry from being applied too many times. A production ledger needs both.

8. How a double-entry system fails and how it is caught

A double-entry ledger is self-checking, but it is not self-correcting, and it does not defend against every kind of mistake. Knowing exactly which failures it catches for free and which it does not is the difference between an engineer who trusts the structure blindly and one who builds the right controls around it.

Failure modes, and what actually catches each one

The pattern across that list is worth stating plainly. Double-entry guarantees internal consistency: the books agree with themselves. It says nothing about external correctness: whether the books agree with reality. A set of books can be perfectly balanced and completely wrong, because every entry was internally valid but some were posted to the wrong place, duplicated, or missing entirely. This is precisely why reconciliation exists as a separate discipline: it is the control that checks the ledger against the outside world, the bank, the custodian, the card network, exactly where the trial balance is silent. The trial balance proves the books are consistent; reconciliation proves they are true.

9. Scaling the ledger from one database to many

For a startup, the entire ledger is a single table in one database, every write runs in one transaction, and the strong consistency you need is free because one database gives it to you. This is the right starting point, and many large fintechs run a remarkably long way on exactly this before they change anything. Do not distribute a ledger before you must.

When volume eventually outgrows one machine, the hard constraint surfaces: a single transaction’s legs must commit atomically (all or nothing), and the system-wide balance invariant must keep holding even as throughput climbs. These are consistency requirements, and a ledger is one of the few systems where you genuinely cannot trade them away. A social feed can tolerate a like that shows up late; a ledger cannot tolerate a debit that posts without its credit, because that is money created from nothing.

flowchart TB
  subgraph one["Stage 1: single ledger DB"]
    A["All accounts, all postings,<br/>one strongly consistent database"]
  end
  subgraph two["Stage 2: partition by account"]
    B["Shard accounts across DBs<br/>keep each transaction's legs<br/>in one shard where possible"]
    C["Cross-shard transactions need<br/>a two-phase commit or an outbox<br/>so all legs commit together"]
  end
  subgraph three["Stage 3: ledger as a dedicated service"]
    D["One ledger service owns the<br/>invariant; other services call it<br/>and never write money directly"]
  end
  one --> two --> three

Several patterns recur as ledgers scale. Partitioning by account keeps most transactions inside a single shard (a transfer between two accounts on the same shard is still one local atomic write), and only genuinely cross-shard transactions pay the cost of a distributed commit. The transactional outbox pattern lets a service write its postings and an outgoing event in one local transaction, so the event is never lost even if the message broker is down. And many firms converge on a dedicated ledger service: a single component that owns the balance invariant and exposes a narrow API, so no other service can write money directly and accidentally violate the rule. The constant across all of these is that the conservation law is non-negotiable. You can shard the storage, you can make balances eventually consistent for reads, but every transaction’s legs must still commit together, because the moment a debit can outlive its credit, you no longer have a ledger.

10. Alternatives and where double-entry ends

Double-entry is dominant, not universal, and a senior engineer should know its edges: what competes with it, what extends it, and where its guarantee stops.

Single-entry still has a place for trivial, low-stakes tracking where the cost of balanced bookkeeping is not worth it, a personal budget, an internal metric. The moment money you must answer for is involved, single-entry is the wrong tool, for all the reasons in section one.

Event sourcing is not an alternative so much as the modern generalization of the same idea. Double-entry already is event sourcing for money: an append-only log of immutable facts from which current state is derived. A system that stores postings and derives balances has independently reinvented the discipline a Venetian merchant would recognize, which is a good sign you are on a solid foundation.

Triple-entry accounting is the genuinely new idea, and it is what blockchains operationalize. In ordinary double-entry, each party keeps its own books, and the books agree only because both parties reconcile against shared reality. Triple-entry adds a third, shared, cryptographically signed record of the transaction that both parties reference, so there is one mutually trusted entry rather than two private ones that must be reconciled. The promise is that reconciliation between counterparties becomes unnecessary because they already share the record. Whether that promise is worth its considerable cost is an active question, but the structural insight is real and worth understanding.

Finally, the honest boundary. Double-entry guarantees internal consistency and gives you a complete, auditable history. It does not value anything (whether an asset is worth what the books say is a separate question of valuation and accounting policy), it does not tell you a posting reflects a real event (that is reconciliation), and it does not decide when revenue is earned or an expense incurred (that is accrual policy). Double-entry is the load-bearing skeleton beneath all of those, the structure that keeps the numbers honest with each other. The rest of this track is what gets built on top of that skeleton, one layer at a time.

Mastery Questions

  1. A junior engineer proposes storing each account’s balance in a column and updating it on every transaction, arguing it makes reading a balance instant and avoids summing a huge postings table. What do you tell them, and is there any version of their idea that is acceptable?

    Answer. The core objection is that an in-place balance is the only record of the truth, and that throws away everything a ledger is for. It has no history, so when it is wrong you cannot see how it got wrong or reconstruct a past balance. It is exposed to lost updates, where two concurrent transactions read the same starting value and one write clobbers the other, silently losing money. And it has nothing to check itself against, so the self-checking property of double-entry is gone. The truth must be the immutable postings, and the balance must be derived from them. There is, however, an acceptable version of their idea: keep a cached or materialized balance for fast reads, as long as it is explicitly derived from the postings and can be recomputed and reconciled against them at any time. The distinction is which one is the source of truth. A cache rebuilt from the log is fine; a balance that is itself the only record is not.

  2. Your ledger’s trial balance nets to exactly zero, every transaction in it is internally balanced, and yet a customer insists money is missing from their account. Is that possible, and how would you investigate?

    Answer. It is entirely possible, because a zero trial balance proves only internal consistency, not external correctness. The books agree with themselves while disagreeing with reality. Several failures produce exactly this: a posting went to the wrong account (a classification error that still balances), a real-world event never produced a posting at all (the bank moved money but no entry was written), or a transaction was duplicated and later partially reversed. None of these breaks the balance check, because each entry is internally valid. The investigation is reconciliation: compare the ledger against an external source of truth, the bank statement, the custodian’s report, the card network’s settlement file, and find the transactions that exist in one and not the other, or that differ in amount. The trial balance can tell you the books are consistent; only reconciliation against the outside world can tell you they are true.

  3. A payment service calls your ledger to move $100, the call times out, and the service retries. Explain precisely why double-entry alone does not prevent a double charge, and what does. Then explain why you still need double-entry even though you have that other mechanism.

    Answer. Double-entry guarantees that each transaction is internally balanced: debits equal credits. The retry, however, produces a second transaction that is also perfectly balanced, so the ledger happily posts it and $200 moves for one real payment. Balancing constrains the shape of a single transaction; it says nothing about how many transactions you posted, so it cannot catch a valid entry applied twice. The mechanism that does is idempotency: the client attaches a unique idempotency key, the ledger records which keys it has applied, and the retry carrying the same key returns the original result instead of posting again, collapsing the duplicate onto the first attempt. You still need double-entry because it defends against a different and equally important failure: a single malformed entry that would create or destroy value. Idempotency stops a correct transaction from happening too many times; double-entry stops an incorrect transaction from happening even once. They are orthogonal controls, and a ledger you can trust in production needs both.

Sources & evidence6 claims · 3 cited

Mix of stable common knowledge and standards. The history (Pacioli 1494, pre-Pacioli Italian merchants) and the accounting equation are well-documented common knowledge; the minor-unit scales are grounded in ISO 4217 and the float-precision claim in IEEE 754. Engineering claims (derived balances, idempotency, append-only postings) are widely accepted industry practice stated as internal reasoning.

  • Luca Pacioli published the first printed description of double-entry bookkeeping (the Venetian method) in his 1494 Summa de arithmetica.verified
  • Double-entry bookkeeping predates Pacioli and was already in use by Italian merchants in the 13th and 14th centuries; Pacioli documented rather than invented it.stable common knowledge
  • The accounting equation states Assets = Liabilities + Equity, extended over time as Assets = Liabilities + Equity + Revenue - Expenses, and every balanced transaction preserves it.stable common knowledge
  • ISO 4217 defines the minor-unit scale per currency: JPY uses 0 decimal places, most currencies use 2, and BHD uses 3.verified
  • Binary floating-point cannot represent most decimal fractions exactly, so 0.1 + 0.2 does not equal 0.3, which is why monetary amounts are stored as integer minor units.verified
  • A double-entry ledger guarantees internal consistency (debits equal credits) but not external correctness; detecting wrong-account, missing, or duplicated postings requires reconciliation against an external source of truth.internal reasoning