Workforce SystemsField engagement

Every screen in the leave system is a view of one balance, so the work was keeping that balance correct through a year of edits, cancellations and resets.

Architecture and delivery lead.
The approach, in four words
entries are the recordthe balance is derivedcorrections are entriesyear-end is a rule
01Entriesthe record
›
02Derivethe balance is a view
›
03Concurrencyenforced at the write
›
04Adjustmentsentries, not edits
›
05Year-enda rule, not a reset
the question is never what the balance is, it is how it got there, and only one of the two models can answer that
What was at stake

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.

How this works

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.

  1. Annual allocation
    the entitlement for the year, as an entry
  2. Request submitted
    pending, so it reduces availability
  3. Request approved and taken
    moves from pending to used
  4. Adjustment with a reason
    a correction is a new line, never an edit
  5. Available is derived
    allocated, less used, less pending
The identity from the specification is the last line, not the first.
FigureA leave balance as a ledger. Each line is an entry with a date and a reason, and the available figure at the end is computed from all of them. Change the holiday calendar or dispute a line, and the balance can be recomputed and explained, because nothing was overwritten on the way.
The constraint

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.

Where it usually breaks

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.

01

Two submissions, one balance

Two requests read the same available figure at the same moment and both succeed. The balance goes below what the policy allows.

02

The retrospective calendar change

A public holiday is added after requests were approved. A stored balance cannot work out which requests it affects, because it never kept them.

03

The silent correction

An administrator overwrites a balance to fix an error. The fix has no reason attached and disappears at the next recalculation.

04

The year-end overwrite

Carry-over is applied by resetting the stored figure, and the previous year's state is gone before anyone checks it.

05

The snapshot mistaken for the record

A cached balance added for speed starts being edited directly, and the system is back to a stored column with a ledger nobody reads.

First request reads available
enough days for this request
Second request reads available
the same figure, a moment later
Both checks pass
each was correct when it ran
Both are written
the balance is now below what the policy allows
The failure point
More leave approved than was available
and nobody did anything wrong
Enforcing the limit at the write makes the second request wait for, and see, the first.
A click-through test never runs two requests at once, so it never finds this.
Ordinary database isolation allows this by default.
FigureHow two correct checks approve more leave than exists. Each request reads the balance before the other has written, so each sees enough days. The fault is not in either check. It is in checking at read time a rule that can only be enforced at write time.
The fork

The reflex, and the fix.

Road not taken

Store the balance and update it

Pull

It is the obvious model, it reads fast, and the arithmetic in the specification is trivial.

Why not

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.

Road taken

Derive the balance from entries

Accepted

An entry ledger and a derivation in place of a column, which is more structure than the arithmetic appears to need.

Bought

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.

Decision

Make the balance derived, because every question that matters about it is a question about how it got there.

How it was built
01Entries
›
02Derive balance
›
03Concurrency control
›
04Adjustments as entries
›
05Year-end
01

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.

02

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.

03

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.

04

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.

05

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.

Inside the key decisions

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.

Stored balance
A column holds the figure
updated in place on every change
Employee disputes it
with a screenshot from earlier in the year
No history to consult
the figure cannot explain itself
Derived balance
Entries with dates and reasons
nothing ever overwritten
Employee disputes it
the same screenshot
Balance recomputed as of that date
the dispute is answered line by line
Same number today, different answer to how
Trust in a balance comes from being able to explain it.
FigureThe same dispute handled by two designs. Both show the same number today. Only one can say how it got there, and in a leave system that is the question every complaint actually asks.
How it was measured

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.

Balance correctness under concurrent submissionReconstructability of a balance at a past dateBehaviour after a retrospective calendar changeTraceability of every adjustment
Under concurrent submissiondoes the limit hold when requests collide
At a past datecan the balance be reconstructed as it stood
After a calendar changeare affected requests re-derived correctly
For every adjustmentis each correction traceable to a reason
Outcomes are described qualitatively. No figures are claimed.
FigureThe four questions the ledger was evaluated against. No measured figures are published for this engagement. Each question is one a stored balance cannot answer by design, which is why they define whether the derivation works.
figures

No measured figures are published for this engagement. Outcomes are described qualitatively.

What it produces
Without this discipline

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.

This system

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.

entries are the recordthe balance is derivedcorrections are entriesyear-end is a rule
The operating envelope

What it owns, and what it hands to a person.

Handled with confidence
Concurrent submissions
Retrospective calendar changes
Administrative adjustment with reasons
Year-end rules
Flagged for review
A rule not expressible as a derivation
Out of scope by design
Policy decisions about entitlement itself
The honest limit

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.

What it generalizes to

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.

What to take from this

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.

How we engage

You have a system like this one.
Tell us where it stands.

Whether it is failing, not yet built, or about to meet a scale it has never seen, we can tell you what we see.

Start a conversation
mostafa@opulion.dev · Response within 24 hours · By inquiry