A leave management system arrived specified as a list of modules and deliverables. Buried in the posting was a worked example that reduced to one arithmetic identity: allocated, less used, less pending, equals available. Every screen in the system is a view of that one number. It is where an employee decides whether they can book a holiday, and where a manager decides whether to approve it. If it is wrong once and nobody can explain why, people stop trusting it and start keeping their own records.
A balance is the sum of its history, and a stored balance has thrown that history away.
Leave arithmetic looks like something you store and update. An employee is allocated some days, uses some, has some requested and waiting for approval, and the rest are available. Write the available figure in a column, subtract when a request comes in, add back when one is cancelled. It works on the first day, and every day after that it depends on every update having been applied exactly once, in the right order, with the right rules.
Banks and accountants solved this problem long ago, and their answer is the ledger. A bank statement is a list of transactions, and the balance is their running total. If the balance looks wrong, you read down the list and find the line. Entries are never erased. A mistake is corrected by a further entry that reverses it, so the mistake and its correction both remain visible. Software engineers call the same idea an append-only ledger, or event sourcing: store what happened, and compute the current state from it.
Storing the history buys time travel. A derived balance can be computed as of any date, which matters because most disputes about leave are about the past. What was my balance when I booked this? What happens to approved requests if a public holiday is added to the calendar? What carried over at year-end, and why? A stored balance only knows its present value, so it can answer none of these.
The last idea is concurrency. Two requests arriving at the same moment can each read the available figure, each see enough days, and each be approved. Neither check was wrong when it ran. Databases, under their usual settings, allow exactly this, because reading a value does not stop someone else from changing it. The only safe place to enforce a limit is at the moment of writing, where the database can make one of the two requests wait for the other.
- Annual allocationthe entitlement for the year, as an entry
- Request submittedpending, so it reduces availability
- Request approved and takenmoves from pending to used
- Adjustment with a reasona correction is a new line, never an edit
- Available is derivedallocated, less used, less pending
A balance stored as a column is a cache with no source behind it. Every correction is an edit, and nothing can reconstruct how the figure reached its value. The failures that matter are all about time: a cancellation after the holiday calendar changed, two submissions arriving together, an administrative adjustment, a year-end reset. A click-through acceptance checklist covers none of those, and all of them happen in an ordinary year.
Leave systems break on time, not on arithmetic.
The arithmetic in a leave system is something a child could do. That is why these systems are tested the way they are: click through each screen, submit a request, approve it, check the number went down. Every test happens today, with one user, in the order the tester chose. The real failures need a calendar, two users or a year.
They arrive late and quietly. A balance drifts by a day after a cancellation was processed against a changed calendar. A manager approves two requests that together exceed what was available. An administrator fixes a mistake by typing over a figure, and the fix is lost at year-end when the reset recalculates from a stale base. Each one is small. Together they produce the moment an employee shows HR a screenshot and nobody can say which number is right.
The reflex, and the fix.
Store the balance and update it
It is the obvious model, it reads fast, and the arithmetic in the specification is trivial.
Concurrent submissions can both read the same available figure and both succeed, corrections become untraceable edits, and a retrospective calendar change cannot be applied because the history it would need was never kept.
Derive the balance from entries
An entry ledger and a derivation in place of a column, which is more structure than the arithmetic appears to need.
A balance that can be reconstructed and explained at any date, corrections that are entries, and concurrency handled at the point where it is actually decided.
Make the balance derived, because every question that matters about it is a question about how it got there.
Entries are the record, the balance is a view
Allocations, requests, approvals, cancellations and adjustments are entries. The balance is computed from them, which makes it explainable as of any date.
Concurrency decided where it is enforced
Two submissions arriving together can both read the same availability under ordinary database isolation and both succeed. The rule that available days cannot be overdrawn is enforced where the write happens, not merely checked where the figure is read.
Corrections are entries, not edits
An administrative adjustment is recorded as its own entry with its reason. The balance still reconstructs, and the correction is visible to anyone who asks.
Retrospective changes are replays
When a holiday calendar changes after requests were approved, the affected entries are re-derived. Nothing is patched by hand, and this is only possible because the entries were kept.
Year-end is a rule, not a reset
Carry-over, expiry and lapse are derivation rules applied at a boundary date. The previous year's state is never overwritten.
What each choice cost, and what it bought.
ADeriving the balance from entries
The specification's arithmetic suggested a column. Building an entry ledger and computing the balance from it was more structure than the arithmetic appeared to need, and every read became a calculation. It bought a balance that can be explained line by line, as of any date, which is the only answer that ends a dispute. Every other step in the engagement depends on this one.
BEnforcing concurrency at the write
A check before saving is the natural place to stop overbooking, and it works in every test anyone runs by hand. It fails when two requests arrive together. Moving the enforcement to the write, where the database can serialise the two, cost some care in how requests are stored. It means the rule holds under load and not only in demonstrations.
CRecording corrections as entries with reasons
Letting administrators edit a balance is simpler, and they will ask for it. Requiring an adjustment entry with a reason adds a step to every correction. It means the correction survives recalculation, appears in the employee's history, and can be audited later by someone who was not there when it was made.
DTreating year-end and calendar changes as derivation rules
Year-end is usually a batch job that resets balances, and a calendar change is usually a manual patch. Expressing both as rules applied to the entries meant writing carry-over, expiry and lapse as logic, and re-deriving affected balances when the calendar changes. Nothing is overwritten, so last year can still be explained this year, and a late calendar change reaches every request it touches.
A single headline number hides where a system fails. This work was scored on the dimensions that actually decide whether it holds in production, measured on real, held-out cases rather than the demo path.
No measured figures are published for this engagement. Outcomes are described qualitatively.
A stored balance that is right on the day it is written, drifts through cancellations and double submissions, and cannot explain its own value when somebody disputes it.
An entry ledger with the balance derived from it, concurrency enforced at the write, adjustments recorded as entries with reasons, and retrospective changes applied by re-derivation.
What it owns, and what it hands to a person.
A derived balance is reconstructable, and it is not free. Deriving costs more than reading a column, which matters at scale and is usually handled with a stored snapshot plus the entries since it was taken. The snapshot is an optimisation of the derivation. Treating it as the record brings back every problem the ledger removed. Policy decisions about entitlement itself, how much leave people should get, sit outside the system.
Any quantity people dispute should be derived from entries. The question asked about it is never only what the balance is. It is how it got there, and only a ledger can answer that.
A balance that people dispute should be computed from entries, the way a bank statement is, and never stored as the only fact.
In a ledger, mistakes are corrected by adding an entry that reverses them. Both the mistake and the correction stay visible.
Keeping the history lets you compute any balance as of any date, which is what retrospective changes and disputes both require.
Checking a limit when reading does not stop two simultaneous requests from both passing. Enforce it where the write happens.
A cached snapshot is a speed optimisation of the derivation. The moment it is edited directly, the ledger stops being the record.