Every team that builds a ledger writes the same check first: before saving a transaction, add up the debits, add up the credits, and refuse the write if they differ. It is the right check. It is also the check that eventually fails, and the way it fails is instructive.
Checks in application code have edges
An application is not one program. It is an API handler, a batch importer written eighteen months later, an admin tool, a migration script that “just fixes the data”, a support engineer with a database client at two in the morning. The balance check lives in the first of those. The others either call it, or they do not.
Nothing about a codebase guarantees that every future path calls the check. A code review can catch one omission. It cannot catch the omission that happens after the reviewer has left. The failure is not a bug in the check; it is that a check which must be invoked is optional by construction.
The same is true of every rule that matters in a ledger. An account must not go below its floor. An entry must never be edited or deleted. One customer’s records must never be visible to another. Each is easy to enforce in the happy path and impossible to enforce in every path, as long as enforcement means “remember to call this”.
What “enforced by the database” actually means
A database rule is different in kind. When the constraint that debits equal credits is attached to the transaction itself and evaluated at the moment of commit, there is no path around it. The API handler cannot skip it; neither can the batch importer, the admin tool, or the engineer with the database client. A write that violates the rule is refused by the storage engine, whatever program attempted it.
Ledger applies this principle to each of the rules above:
- Balance is verified per asset when the transaction commits. An unbalanced write cannot exist, through any path.
- Floors are a constraint on the balance table itself. Even code that bypasses the normal posting route cannot push an account below its floor, because the storage engine will not store the row.
- Immutability is not an access control that could be misconfigured. There is no edit or delete path on the journal at all, and a guard refuses the operation independently of who asks.
- Isolation is enforced by row-level policies inside the database that fail closed: a request with no valid boundary gets an error, not somebody else’s rows.
The application still checks these things first, because a friendly error at the API is better than a terse one from the database. But the application check is a convenience. The database rule is the guarantee.
Why this matters to the people who sign off
For a finance leader the difference is auditability. When a rule is enforced by the database, the statement “the books cannot be out of balance” is not a description of how the team works; it is a property of the system that can be demonstrated to an auditor by trying to break it and failing.
For an engineering leader the difference is what happens in year three. Teams change, the codebase grows paths nobody planned, and a rule that must be remembered will, eventually, be forgotten once. A rule the database enforces does not depend on anyone remembering it.
What it costs
There is a cost, and it is honest to name it. Rules in the database are harder to change, and that is partly the point: a financial rule should not change casually. They also require the database to do more work at commit time, which is why a ledger designed this way is deliberate about locking, ordering and the size of a transaction.
A worked example: the path nobody remembered
A team we spoke to had a wallet service with a careful balance check in its API. Eighteen months in, a data-migration script was written to move balances between two account structures. It updated the balance table directly, because that was the fastest way to do it, and it ran cleanly. Three weeks later the reconciliation report showed the customer liability was higher than the sum of the entries by an amount nobody could explain. The migration had moved balances but not the entries behind them; the check in the API had never been in its path.
Nothing in that story is unusual. The people were competent, the check was correct, and the failure was structural: enforcement that has to be invoked can be skipped by a program that does not know it exists. Had the balance been a value derived from the entries under a database constraint, the migration would have failed on its first row, loudly, in the developer’s terminal.
How to check this in a vendor
Whatever ledger you evaluate, ask three questions and expect concrete answers:
- Where is each rule enforced? “In our service layer” means it depends on the next release. “In a database constraint or trigger” means it holds for every path.
- Show me the rule refusing a write. Ask to see a direct write that violates the rule — an unbalanced transaction posted with a database client, not through the API — and watch it fail.
- Which check guards it? A rule without a test that has been seen to fail is a description, not a guarantee.
Ledger’s answers are on the guarantees page: each rule names its enforcement and the live probe that guards it, and the audit history records the rounds in which those probes were made to fail before they were trusted to pass.
Ledger accepts those costs. Every guarantee on the guarantees page names how it is enforced and which live check guards it, so the claim can be examined rather than taken on trust. If you would like to try to break one, request access and we will give you a tenant to do it in.