Founder & Lead Engineer, RAITHub
RAITHub ships and tests production software. See QA as a Service or talk to us.
When a double-entry ledger stops balancing, something wrote a transaction whose debits and credits do not sum to zero. Do not guess, and never edit or delete past entries. Find the day the balance first broke, trace it to the offending transaction, correct it with a new adjusting entry, and then make it impossible to recur by enforcing balance in the database itself.
If you would rather have the ledger audited and repaired for you, see how RAITHub would fix it at the end of this guide.
What does "out of balance" actually mean?
In double-entry accounting, every transaction moves value between accounts so that debits equal credits. The whole ledger sums to zero, and each transaction sums to zero on its own. "Out of balance" means that invariant has broken: a transaction posted with one leg missing, a wrong amount, a currency mixed in, or an update that touched one side and not the other. The amount of the imbalance is a clue, not the problem itself.
| Cause | Signature | Where to look |
|---|---|---|
| Missing leg | Imbalance equals one real transaction amount | A code path that inserted a debit without its credit |
| Wrong amount on one leg | Small, odd imbalance | Rounding, or a fee split that does not add up |
| Edited history | Imbalance with no matching new entry | Someone ran an UPDATE or DELETE on posted entries |
| Currency mixed in | Imbalance equals an FX difference | Entries in two currencies summed as if one |
| Partial write | Imbalance after a crash or timeout | A transaction that was not atomic |
If the symptom is that your ledger disagrees with the gateway or bank rather than with itself, that is a reconciliation mismatch, not an out-of-balance ledger: start with finding and fixing a reconciliation mismatch.
How do I find the day the ledger went out of balance?
Binary-search by date. Compute the running imbalance at the end of each period and narrow until you have the first day it broke. From one day, you are looking at a handful of transactions, not a year of them.
-- Daily net of every entry. A zero-sum ledger should return 0 every day.
SELECT entry_date::date AS day,
SUM(amount_minor) AS net_minor -- debits positive, credits negative
FROM ledger_entry
GROUP BY entry_date::date
HAVING SUM(amount_minor) <> 0
ORDER BY day;
-- Within the first broken day, find transactions whose legs do not net to zero.
SELECT transaction_id,
SUM(amount_minor) AS txn_net_minor,
COUNT(*) AS legs
FROM ledger_entry
WHERE entry_date::date = DATE '2026-10-03'
GROUP BY transaction_id
HAVING SUM(amount_minor) <> 0;
The second query is the one that matters: a correct transaction nets to zero across its legs. Any transaction that does not is your culprit, and the legs count tells you whether a leg is missing (often one leg where there should be two) or an amount is wrong (two legs that do not cancel). This assumes amounts are stored as integer minor units, which is the only safe way to hold money, explained in double-entry ledger database design.
How do I repair it without destroying the audit trail?
With an adjusting entry, never an edit. A ledger is a legal and operational history; editing it means every report that already ran is now wrong, and you can never prove what the books said before. The correct repair posts a new, balanced transaction that references the broken one.
- Reconstruct what should have happened. From the source event (a charge, refund or transfer), work out the correct legs and amounts.
- Post a correcting transaction that, added to the broken one, produces the right balances. Reference the original transaction ID in the correction's description.
- Do not delete the original. The error and its correction both stay in the history, which is exactly what an auditor wants to see.
- Re-run the daily check and confirm the imbalance is gone from that day forward.
- Record who corrected it and why. The correction is itself an audited event.
Immutability is not optional for a ledger. Square's engineering write-up describes building an immutable double-entry accounting database, and the principle is the same at any scale: you append corrections, you never rewrite.
How do I make an unbalanced transaction impossible to commit?
Enforce the zero-sum rule in the database, inside the same transaction that writes the legs, so a bug can never leave the ledger broken. A deferred constraint trigger that checks the balance at commit time does this.
-- Reject any transaction whose legs do not net to zero, at COMMIT.
CREATE CONSTRAINT TRIGGER ledger_must_balance
AFTER INSERT ON ledger_entry
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
EXECUTE FUNCTION assert_transaction_balances();
CREATE FUNCTION assert_transaction_balances() RETURNS trigger AS $func$
BEGIN
IF (SELECT SUM(amount_minor)
FROM ledger_entry
WHERE transaction_id = NEW.transaction_id) <> 0 THEN
RAISE EXCEPTION 'ledger transaction % does not balance', NEW.transaction_id;
END IF;
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
Because the trigger is DEFERRABLE INITIALLY DEFERRED, it fires once at commit, after all legs are inserted, so a normal two-leg write passes while a transaction left unbalanced is rejected and rolled back. PostgreSQL's CREATE TRIGGER documentation covers constraint triggers. Pair it with writing all legs in one database transaction, so a crash can never leave half a transaction behind.
Buy, build or hire?
| Option | Example | Choose this when | Where it stops |
|---|---|---|---|
| Accounting software | A general ledger package | You are tracking company books, not in-app balances | Not built into your product's transaction flow |
| Managed ledger engine | TigerBeetle or Modern Treasury | You want double-entry guarantees without building them | Another system to integrate and operate |
| Build with DB constraints | Your own schema plus the balance trigger | The ledger is core to your product and must stay in your database | You own the invariants and the tests |
| Hire an engineering team | Audit, repair, then enforce | The ledger is broken now and you cannot trace it | Not a substitute for an accountant's sign-off |
How long does it take to fix yourself?
Our estimate, for a developer who knows SQL and the schema: a few hours to a day to binary-search to the broken day and trace the offending transaction; a day or two more to post corrections and add the balance trigger and tests. The main risk of doing it alone is editing history to "make it balance", which hides the error, breaks past reports, and is the one move an auditor will never forgive.
How RAITHub would fix this
- Audit: binary-search the imbalance to the first broken day and the exact transactions.
- Repair forward: correcting entries that reference the originals, with the history left intact.
- Enforce: a deferred balance constraint plus atomic multi-leg writes so an unbalanced transaction can never commit.
- Prove it: tests that attempt unbalanced, missing-leg and partial writes and assert each is rejected.
Timeline: an audit and repair is days to a week; hardening the ledger with constraints and tests is 2–4 weeks as a code-rescue or backend engagement. What you receive: automated tests and CI, handover docs, and full IP under NDA. Production and financial data stay in your own cloud account; development uses synthetic data.
RAITHub is an engineering studio, not an accounting firm, and has not shipped a regulated or licensed fintech product; how the books are kept for tax and audit is a question for your accountant, and this is general information, so confirm with your adviser. Proof we can point to: Sundor Skin enforces money and credit rules in PostgreSQL across 146 tables under 530+ automated tests, and TheSkinProof, the founder's own venture, runs multi-gateway payments with 750+ tests. Next step: a free 15-minute technical audit, then a written fixed quote. Book the audit. More is on the SaaS development page and the FinTech page.
Frequently asked questions
What does it mean when a double-entry ledger is out of balance?
A transaction posted whose debits and credits do not sum to zero: a missing leg, a wrong amount, a currency mixed in, or an edited past entry. Each transaction should net to zero on its own, and the whole ledger should too.
How do I find where the ledger broke?
Binary-search by date. Compute the net of all entries per day; a zero-sum ledger nets to zero every day. On the first broken day, group by transaction and find the ones whose legs do not net to zero. The leg count tells you missing-leg from wrong-amount.
Can I just edit the wrong entry to fix the balance?
No. A ledger is append-only history. Editing it breaks every report that already ran and destroys your ability to prove what the books said. Post a correcting transaction that references the original; keep both.
How do I stop it going out of balance again?
Enforce the zero-sum rule in the database with a deferred constraint trigger that checks balance at commit, and write all legs of a transaction atomically so a crash cannot leave half a transaction. Then test that unbalanced writes are rejected.
Is this the same as a reconciliation mismatch?
No. Out of balance means the ledger disagrees with itself. A reconciliation mismatch means the ledger disagrees with the gateway or bank. They have different causes and fixes, though a bad correction can cause one while fixing the other.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.