Guide

A ledger is the system of record for money inside your product. It answers "who moved what, and why?" with a permanent trail. The method is double-entry bookkeeping. Every movement is a journal with equal debits and credits. Money is never created or destroyed inside the books. It only moves from one account to another.

The big idea

Each journal has at least two entries. One debits an account, and one credits another, and the two amounts are equal. If the books stop balancing, something is wrong. The check is built into the design.

Entries are never edited. A mistake is fixed by posting a new journal that reverses it. That keeps the history honest, which is what auditors need.

Walking through it

A customer pays $20.00 for order 55. Amounts are in cents, so that is 2000:

Journal j_1001 (payment of 2000 cents):
  DR  cash_psp_clearing     2000
  CR  revenue_orders        2000

Refund of $5.00, as a new journal, not an edit:
Journal j_1002:
  DR  revenue_orders         500
  CR  cash_psp_clearing      500

After both journals, revenue_orders holds 2000 − 500 = 1500 cents, which is $15.00. Both journals stay in the history.

Here is the same idea for a later refund. A $25.00 capture (2500 cents) and then a $10.00 refund (1000 cents) leave 1500 cents available to refund. That number is computed from the journals, not stored in a mutable column.

The properties that matter in engineering:

  • Immutable entries. Never update a posted amount. Reverse it with a new journal.
  • Derived balances. A balance is the sum of entries for an account. If you maintain a cached balance, update it in the same transaction as the insert.
  • Integer minor units. Store amount_cents as a bigint plus a currency code. Never store floats.
  • Append-only tables. A journals table and an entries table, with entries pointing at journals.

Refunds and payouts are journals too. A refund reverses revenue. A marketplace payout moves money from the platform's receivable to the seller's payable, then to the PSP. Never write "seller_balance += x" as the only record.

Trade-off

Storing only the PSP's state is faster to build, but it fails audits, disputes and marketplace payouts. A full ledger with PSP references costs schema and operational discipline. It becomes the default once two parties' money, or an audit, is involved.

Square's Books service is a public example of an immutable double-entry store, built for the same reasons at marketplace scale.

Common mistakes

  • Updating a posted amount. It erases what happened, and the audit trail breaks.
  • Storing money as a float. Rounding errors appear as small drifts.
  • Keeping only a balance column. Without journals, you cannot explain how the balance changed.

Check yourself

  1. A $25.00 capture is followed by a $10.00 refund. How much is available to refund?

    Answer: 1,500 cents ($15.00), computed from the journals.

  2. Why reverse an entry with a new journal instead of editing it?

    Answer: the history stays intact, and auditors can follow every change. Editing a posted amount erases what happened.

Key takeaways

  • Every money movement is a balanced, immutable double-entry journal.
  • Store money in integer minor units, and never use floats.
  • Derive balances from journals, and correct mistakes with reversing journals.
  • Refunds and payouts are journals too.

Further learning

Square's Books service describes an immutable double-entry store used at marketplace scale:

Square: Books, an immutable double-entry accounting database serviceTracking financial transactions at scaleSquare Corner Blog

Next, see how the ledger stays in step with the payment provider, in webhooks and outbox.