27 August 2026

What 'Double-Entry Enforced at Three Layers' Actually Means

Double-entry accounting has one rule that matters more than any other: every journal entry’s debits must equal its credits. Break that rule even once, silently, and every report built on top of the ledger afterward is wrong in a way that’s hard to trace back to its source. Most software enforces this rule exactly once — usually a check in the application code right before it writes the entry. Rechvix enforces it three times, in three different places, on purpose.

Layer 1 — the application-level sum check

Before any journal entry is submitted to the database, the accounting module sums every debit line and every credit line using the Money type described in the previous postand rejects the operation outright if they don’t match exactly. This is the layer that catches the overwhelming majority of bugs, cheaply, before a single row is written.

Layer 2 — a deferred database constraint trigger

The application layer is still just Go code, and Go code can have bugs — a missed code path, a new feature that constructs a journal entry a different way, a future contributor who doesn’t know about the sum check. So the database itself carries a second, independent check: a deferred constraint trigger that runs at transaction commit time and re-sums every debit and credit for the journal entry being posted, rejecting the commit if they don’t balance.

CREATE CONSTRAINT TRIGGER journal_entry_must_balance
  AFTER INSERT OR UPDATE ON journal_lines
  DEFERRABLE INITIALLY DEFERRED
  FOR EACH ROW EXECUTE FUNCTION check_journal_balance();

"Deferred" matters here — a journal entry is written as several line-item rows in one transaction, and you can’t check the balance until every line has been inserted. Deferring the check to commit time lets the transaction write its rows in any order and still get checked as a whole before it becomes durable.

Layer 3 — no UPDATE grant on posted rows

The first two layers stop an unbalanced entry from ever being created. The third stops a balanced entry from being silently edited afterward, which would be just as damaging — a correct entry that gets quietly changed later is indistinguishable from fraud or a very expensive bug. The database role the application connects as simply has no UPDATE privilege on posted journal-line rows. Correcting a mistake means posting a new, explicit reversing entry — which is also how real accountants are trained to do it — not silently rewriting history.

Why three layers, not one

Because each layer fails differently, and independently. A bug in the Go sum-check logic doesn’t help you if the database trigger is still there to catch it. A missing trigger after a migration mistake doesn’t matter if the application check already rejected the entry. And even if both checks somehow had the same bug at the same time, there is still no way to edit a posted entry to hide the evidence — only to reverse it, on the record. A single point of enforcement is a single point of failure; three independent ones, checking the same invariant in different ways, are not.