Skip to content
Engineering

Double-Entry Ledgers for Payments: Designing the Books Behind Your Money Flows

Once your product holds or moves money for other people, a status column isn't enough: you need a ledger. Double-entry bookkeeping explained for engineers, the accounts a payments business usually needs, a worked marketplace example from payment to payout, a schema that enforces balanced entries, and the rules that keep a ledger trustworthy.

LILPE Editorial Team9 October 2026 8 min read
Cover for an engineering guide to designing a double-entry ledger for payments

Most products start by tracking money the obvious way: a payments table with a status, a balance column on the user, perhaps a payouts table. It works until it doesn't. A balance drifts and nobody can explain why. A refund is applied twice. Finance asks how much the business owes sellers today and gets three answers. These aren't bugs in one query; they come from storing results instead of the movements that produced them.

A double-entry ledger records every movement of money as balanced entries between accounts, and derives every balance from those entries. It's the same idea accountants have used for centuries, and it suits software remarkably well. This guide explains it for engineers building wallets, marketplaces, payouts or anything else that holds money for others. It's a design guide, not accounting or tax advice; involve your finance team in the chart of accounts.

Why statuses and balance columns aren't enough

A balance column is a cached answer with no working shown. When it's wrong, there's nothing to check it against. A ledger turns that around:

  • Every balance can be recomputed from entries, so a wrong balance can be traced to the entry that caused it.
  • Nothing is overwritten. History is the record, which is what auditors, support teams and regulators need.
  • Every movement has two sides, so money can't appear or vanish: if a seller's balance went up, some other account went down by the same amount.
  • Questions like "how much do we owe sellers?" or "how much is the provider still to pay us?" become the balance of one account.

Double-entry in one paragraph

Money is held in accounts. Every movement is a transaction made of two or more entries, each moving an amount into or out of one account, and the entries of a transaction always sum to zero. Using signed amounts, a transfer of ₹100 from account A to account B is two entries: minus 100 on A, plus 100 on B. Accountants call the two sides debits and credits, and whether a debit increases or decreases an account depends on its type (assets and expenses increase with debits; liabilities and revenue increase with credits). Many engineering teams store signed amounts and let reports present them as debits and credits.

Accounts a payments business usually needs

Your chart of accounts will be specific to your business, but these appear in most payment flows:

Typical ledger accounts in a payments product
AccountTypeWhat its balance means
Bank (operating or nodal/escrow)AssetMoney actually in the bank account
Provider receivableAssetCaptured payments the provider hasn't settled yet
Seller or customer payableLiabilityMoney you hold that belongs to sellers or customers (wallet balances)
Refunds payableLiabilityRefunds approved but not yet paid out
Commission or fee revenueRevenueWhat you've earned
Provider feesExpenseWhat payment providers charged you
SuspenseAsset or liabilityMoney you can't yet attribute; should be worked to zero

A worked example: one marketplace order

A customer pays ₹1,000 for a seller's product on a marketplace. The marketplace keeps a ₹100 commission. The payment provider charges a ₹20 fee and settles the rest. Then the seller is paid out. GST is left out to keep the arithmetic readable; in practice it gets its own accounts. Signed amounts: a positive number increases an asset or expense account, a negative number increases a liability or revenue account.

Ledger entries for one ₹1,000 marketplace order (illustrative amounts)
EventAccountAmount (₹)
1. Payment capturedProvider receivable+1,000
1. Payment capturedSeller payable−1,000
2. Commission chargedSeller payable+100
2. Commission chargedCommission revenue−100
3. Provider settles, net of feeBank+980
3. Provider settles, net of feeProvider fees+20
3. Provider settles, net of feeProvider receivable−1,000
4. Seller paid outSeller payable+900
4. Seller paid outBank−900

Each event sums to zero. Afterwards, provider receivable is back to zero (the provider has paid everything it owed), seller payable is zero (the seller has been paid), the bank holds ₹80 more than before, commission revenue shows ₹100 earned, and provider fees show ₹20 spent. The ₹80 in the bank is exactly revenue minus fees. If any of those balances were different, you'd know precisely which step to look at.

A schema that enforces the rules

A minimal ledger has three tables: accounts, transactions (a journal of events) and entries. The important part is what the database guarantees.

SQLPostgreSQL: a minimal append-only ledger
CREATE TABLE ledger_accounts (
  id        bigint  GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  code      text    NOT NULL UNIQUE,          -- e.g. 'seller_payable:S123'
  type      text    NOT NULL CHECK (type IN ('asset','liability','revenue','expense')),
  currency  char(3) NOT NULL
);

CREATE TABLE ledger_transactions (
  id            bigint      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  kind          text        NOT NULL,         -- 'capture', 'settlement', 'payout', ...
  external_ref  text        NOT NULL,         -- provider or internal reference
  effective_at  timestamptz NOT NULL,         -- when it happened in the real world
  recorded_at   timestamptz NOT NULL DEFAULT now(),
  UNIQUE (kind, external_ref)                 -- the same event can't be posted twice
);

CREATE TABLE ledger_entries (
  id              bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  transaction_id  bigint NOT NULL REFERENCES ledger_transactions (id),
  account_id      bigint NOT NULL REFERENCES ledger_accounts (id),
  amount_minor    bigint NOT NULL CHECK (amount_minor <> 0)  -- signed, in paise
);

-- Post a transaction and its entries inside one database transaction, and
-- check before committing that its entries sum to zero per currency.
-- Never UPDATE or DELETE entries: grant the application INSERT and SELECT only.

The UNIQUE constraint on kind and external reference makes posting idempotent: if the same settlement file or webhook is processed twice, the second insert fails instead of doubling the money. Checking that entries sum to zero can be done in the posting function, or in the database with a deferred constraint trigger, but it must happen in the same database transaction as the inserts.

Rules that keep a ledger trustworthy

The schema is the easy part. These rules are what make the numbers believable:

  1. Append-only. Never edit or delete an entry. Fix a mistake with a reversing transaction that cancels it, then post the correct one. The mistake and its correction both stay visible.
  2. Every transaction balances. No exceptions, no "temporary" one-sided entries.
  3. Idempotent posting. Each real-world event maps to exactly one transaction, keyed by its reference.
  4. One currency per account. Currency conversion is a transaction between accounts in different currencies, with the rate recorded, never a change of currency on an existing balance.
  5. Integers in minor units. Amounts in paise or cents as whole numbers, never floating point. Our guide to storing money in code explains why.
  6. Two dates. When something happened (effective) and when you recorded it. Reports for a period use the effective date; audits need both.
  7. Balances are derived. Cache them for speed if you must, but they're always recomputable from entries, and you should regularly check that they match.

Balances, concurrency and overdrafts

Reading a balance is a sum over an account's entries. For accounts with many entries, keep periodic balance snapshots and sum only entries after the latest one.

Wallets and seller balances add a constraint: a debit mustn't take a balance below zero. Two concurrent payouts can each see enough balance and both proceed. Prevent it by locking the account row (for example with SELECT ... FOR UPDATE in PostgreSQL) while you check the balance and post the debit, so the second payout waits and then sees the reduced balance. Keep these transactions short, and always lock accounts in a consistent order to avoid deadlocks.

Pending and posted money

Not every movement is final when you learn about it. A card authorisation reserves money that may never be captured; a payout may be sent but not yet confirmed. Model these explicitly, either as separate pending accounts or as a status on the transaction, so available balance (what can be spent or paid out) and ledger balance (everything posted) are both answerable. Moving from pending to posted is itself a transaction.

How the ledger helps reconciliation

A ledger makes reconciliation precise. In the example above, provider receivable should return to zero once a payment is settled; any amount left in it after the expected settlement date is a payment the provider hasn't paid or one you haven't matched. The same goes for refunds payable, payouts in transit and suspense. Reconciliation becomes a matter of explaining every non-zero balance in accounts that should be clear. Our payment reconciliation guide covers the matching itself.

Common mistakes

The ones we see most often:

  • Storing a balance column and "also" keeping a ledger, then trusting the column when they disagree.
  • Netting fees into the payment amount, so the gross amount and the fee can never be reported separately.
  • Posting from webhooks without an idempotency key, so a retried event posts twice.
  • Using floating-point amounts, or mixing currencies in one account.
  • Editing entries to fix mistakes, which destroys the audit trail.
  • Leaving suspense to grow; it should be worked down daily.

When you need one

You need a proper ledger once your product holds money on behalf of others or owes money to them: wallets and stored balances, marketplaces that split payments and pay out sellers, platforms that pay out from a balance, or any product that gives credits or refunds into a balance. Some of these, such as wallets that let customers store value, are regulated activities in India that need RBI authorisation, so check your model with counsel before building. If you're splitting payments between sellers, see our marketplace payments guide; for matching the provider side, LILPE's reconciliation page describes how settlements are matched to payments and bank credits.

Frequently asked questions

What is a double-entry ledger?

A way of recording money in which every movement is a transaction of two or more entries between accounts, and each transaction's entries sum to zero. Balances are calculated from the entries, so every balance can be explained and money can't appear or disappear without a matching entry elsewhere.

Why do fintech and payment products use double-entry ledgers?

Because they hold or move money for other people. A ledger gives a complete, append-only history, balances that can be recomputed and explained, protection against double posting, and clear answers to questions such as how much is owed to sellers or still due from a provider.

Should ledger amounts be positive and negative or debits and credits?

Either works if it's consistent. Many engineering teams store signed amounts that sum to zero per transaction and present them as debits and credits in reports. What matters is that every transaction balances and the sign convention per account type is documented.

How do you correct a mistake in a ledger?

Post a reversing transaction that cancels the wrong one, then post the correct transaction. Never edit or delete existing entries; the mistake and its correction both stay in the history.

How do you stop a wallet balance going negative?

Lock the account while checking its balance and posting the debit, in one database transaction, so concurrent debits are applied one after another. Without a lock, two payouts can both see enough balance and both go through.

Official sources

Last reviewed . Examples, amounts and screens marked illustrative are not LILPE figures.

Route every payment to where it succeeds.

Tell us which providers you use today and where payments get stuck. We'll show you what the setup would look like on LILPE.