Double-Entry for Engineers

Double-entry accounting is an invariant system: every transaction writes two or more entries whose signed sum is zero, so the books cannot silently lose money — only visibly misplace it. For engineers it is best understood as a five-hundred-year-old consistency protocol that survived because it assumes humans make mistakes.

The translation that makes it click

Forget debit and credit as vocabulary. They are directional labels with historical baggage, and the baggage is what makes engineers bounce off the topic.

Think instead of signed flows between accounts, with one hard rule: the flows in any single transaction sum to exactly zero.

A customer pays ₹1,000 through a gateway that charges ₹20:

  • Gateway receivable: +1,000 — they now owe you this
  • Customer receivable: −1,000 — their obligation is discharged
  • Fee expense: +20
  • Gateway receivable: −20 — they will net this from settlement

Sum: zero. Not by convention, not by review — by construction, enforced at the write.

That is the whole idea. Accountants discovered defensive programming in 1494, Luca Pacioli wrote the specification, and the industry has been shipping it in production for five centuries with a remarkably good incident record.

Why single-entry always fails eventually

A single row — “received ₹1,000” — records what happened and nothing about the relationship it changed.

That is adequate right up until you hold value belonging to someone else. A wallet balance. A marketplace seller’s earnings. A customer deposit. Subscription credits. The moment that happens, the questions become relational, and they are the questions that matter most:

How much do we owe this seller right now? What portion of our bank balance is genuinely ours versus held on behalf of users? If everyone withdrew today, could we honour it?

Double-entry answers all three structurally, because every unit of value is always in an account and always arrived from another account. Single-entry answers them with a shrug and a query somebody wrote once.

There is a second reason, less discussed and equally decisive: auditors’ methodology assumes it. Their sampling procedure is to pick an entry and trace both sides. In a single-entry system that procedure has nowhere to go, which turns a routine audit into an investigation — and investigations are expensive whether or not anything is wrong.

The engineer’s checklist

Seven things, each of which prevents a specific class of pain:

Integers in minor units. Paise, fils, cents. Floating point near money is a trivia fact until it is a recurring three-paise reconciliation difference that costs a day to trace and returns monthly.

Append-only, always. Corrections are reversing entries, never edits. An auditor’s instinct is to check for deletions, and finding evidence of them reframes every other answer you give.

Enforce zero-sum at the database. Not in application code. Two concurrent requests can both pass an if-statement; only the database resolves the race. This is the most common implementation mistake and it produces bugs that appear only under load.

Account granularity that mirrors reconciliation needs. One giant “cash” account turns “we are short by ₹40,000” into an investigation. Per-gateway receivables turn it into “the Razorpay Tuesday file is short”, which is a sentence you can act on.

Two timestamps: business time and record time. When it was agreed versus when value moved. Neither is derivable from the other, and the entire T+1 settlement problem lives in that gap.

One currency per account. Cross-currency movements post through an explicit FX account so the rate used is recorded data rather than an implied number nobody can audit.

Balances as projections. Computed from entries, cached for speed, rebuildable from scratch. If you cannot rebuild it, it is not a cache — it is a second source of truth that will eventually disagree with the first.

What you get once it is in place

Structural honesty. You cannot silently lose money. You can misplace it — post to the wrong account, misclassify a fee — but the total is conserved, so every error is findable. That is a categorically different failure mode from “the number is wrong and nobody knows why”, which is the state most single-entry systems eventually reach.

Replay. Rebuild any projection from history. A bug in the balance calculation becomes a code fix and a recompute rather than a data migration.

Time travel. What was this account’s balance on 3 March? A query. This matters more than teams expect, because it is what stops last quarter’s reported numbers quietly changing when somebody fixes something — the single most corrosive experience a finance team can have with a system.

Audit as query. “Show me every fee this customer was charged, with sources.” Seconds, with evidence attached. In India this has moved from nice-to-have to operationally necessary — the digital lending framework’s fee-transparency expectations assume systems that can do exactly this on demand.

The objection worth taking seriously

It is more work up front. Two or more rows instead of one, a chart of accounts to design, an invariant to enforce, and a mental model the team has to learn.

That is all true, and the honest response is that the cost is front-loaded and small while the alternative cost is back-loaded and unbounded. Ledger drift does not announce itself; it accumulates quietly and presents its invoice at an audit, a funding round or a regulatory query — precisely the moments when you can least afford an archaeology project.

The teams I have watched regret this decision are, without exception, the ones who chose the balance column.

Designing the chart of accounts

This is the part that needs judgment rather than engineering, and it is where I see the most avoidable pain.

Start from your reconciliation questions. Every external party you must agree with deserves its own account — each gateway, each bank account, each partner. If you will one day need to answer “does our record of Razorpay match theirs”, that requires an account whose balance means exactly that.

Separate held money from owned money, structurally. Customer wallet balances, seller payables and deposits are liabilities you happen to be holding, and mixing them with revenue in one account is how businesses discover they have been spending float. The account structure should make that mistake difficult rather than merely discouraged.

Model fees where they are incurred, not where they are noticed. A gateway fee netted at settlement is still an expense at transaction time; recording it only when the settlement arrives makes per-transaction economics unavailable.

Leave room for suspense, and age it. Money arrives that cannot yet be attributed, and it needs somewhere balanced to sit. The rule that keeps suspense honest: every entry carries an expected resolution date, and the balance is a report somebody owns. A suspense account nobody ages becomes the permanent home of unexplained money, which defeats the entire purpose.

A chart designed this way takes an afternoon with someone who understands the business, and it survives years. One designed by copying a template survives until the first question it cannot answer.

/blog/ledger-design — the schema, in detail · /blog/why-your-ledger-drifts — what skipping it costs · /blog/idempotency-done-right — protecting the write path · /services/fintech — having it built · /products/ledger-platform — the foundation, pre-built

Questions I actually get

Do I need an accountant to implement this?

No, but you need one to review your chart of accounts, which is where domain judgment actually lives. The mechanics are an engineering concern; deciding what accounts exist and what they mean is a business one, and getting it wrong is expensive to unwind later.

Is double-entry overkill for a small product?

If money moves through you and you hold value for anyone else, no. The moment you have wallets, seller balances, deposits or credits, single-entry cannot answer whose money something is — and that question always eventually gets asked, usually by someone with authority.

How do I explain this to a team that has never used it?

Drop the debit and credit vocabulary and talk about signed flows between accounts, with a per-transaction invariant that sums to zero. Engineers get it in about ten minutes when it is framed as a constraint rather than as accounting tradition.

What about performance? Two rows instead of one.

Two or more rows instead of one, and it is rarely the bottleneck. Postgres handles the write volume comfortably with partitioned entries, and the projections that serve reads are cached. The cost is real and small; the benefit is that errors become findable.

Can I migrate to double-entry from a single-entry system?

Yes, alongside rather than in place: post to the new structure in parallel, reconcile the two daily, and cut over once the difference holds at zero. That is the standard migration method and it does not require freezing the business.

Where do most engineers go wrong first?

Enforcing the zero-sum invariant in application code rather than in the database. Two concurrent requests can both pass an if-statement, and the resulting imbalance is exactly the kind of rare, load-dependent bug that surfaces in production and nowhere else.