Threads · 11 / 18 · Sep 7, 2026 · Apache-2.0
The append-only table that blocked its own correction
Forbidding modifications is right, and it holds until the day you need to fix an entry that is wrong — and discover your own rule standing in the way of a correction everybody agrees with. What happens next in most systems is a "temporary" admin script with the constraint switched off. From that day the table is append-only in prose only.
Enforced, not promised
expect(() => l.post([{ debit: "10.00", credit: "0.00", … }])).toThrow(OutOfBalance);
"Entries always sum to zero" written in a document is a hope. Here it is a rule that refuses, and the error
says by how much. Same for immutability: update and delete exist only so the tests can prove they are
refused.
The moment this is named after
A payment of 40 was posted as 400.
expect(l.balance("bank")).toBe("400.00");
expect(() => l.update("e1-a", { debit: "40.00" })).toThrow(Immutable);
expect(() => l.delete("e1-a")).toThrow(Immutable);
The rule is doing exactly what it was built to do, and it is standing between you and a fix nobody disputes. This is the point where the invariant usually dies — not to an attacker, to a Tuesday.
The correction is another entry
l.reverse("e1-a", "reversing: amount keyed wrong");
l.post([...correcto]);
expect(l.balance("bank")).toBe("40.00"); // right answer
expect(l.find("e1-a")!.debit).toBe("400.00"); // and the mistake is still there
The history grows; it never shrinks. Four entries where an UPDATE would have left two:
expect(l.all()).toHaveLength(4);
Somebody auditing this can see what was posted, that it was wrong, when it was noticed, and what replaced it. An update leaves the right number and no way to learn any of that — and no way to tell a correction from a quiet edit.
Two details that decide whether people keep using it
Reversing twice is refused. A second reversal is a second mistake, not a correction, and the store says so rather than quietly producing an entry that cancels a cancellation.
The reversal is an ordinary posting. It sums to zero like everything else and goes through the same door:
expect(total).toBe(0);
If the correction needed an exception to the rules, the exception would be the new way in — and the invariant would be gone within a quarter.
What to carry over to a real database
Say it in the schema, not in a comment:
REVOKE UPDATE, DELETE ON ledger_entries FROM app_user;
ALTER TABLE ledger_entries ADD CONSTRAINT balanced CHECK (…);
REVOKE is what makes it true for every path, including the console somebody opens at midnight, and it is
what forces the reversal design to exist before it is urgently needed rather than after.
Where this comes from
niiko keeps a double-entry ledger where money is concerned, and both halves of this page are from it: the rule, and the afternoon it refused a correction that had to be made.
License
Apache-2.0 — see LICENSE. This is a demonstration, not a package. Copy what you need.
Built by Vorluno — a software studio from Panamá.
// next threadThe key identifies the effect, not the occasion