Skip to content

Writing

Snapshot the approver, not the role

The approval record stored a role. Looking it up a year later returned whoever currently holds that role, which produced an answer that was confident, plausible, and wrong.

The question

Somebody asked who had been entitled to approve a particular payment, several months earlier.

It is a reasonable question. It comes up in audits, in disputes, and any time somebody wants to understand how a decision was reached.

The system answered immediately and the answer was wrong.

Why it was wrong

The approval record stored the fact that the approval had happened, and it stored the role that had been required. Resolving that role at read time returned whoever holds it now.

Two people had changed jobs in the intervening months. The person the system named had not been in that role in March. The person who had actually approved it was no longer in it.

The failure is subtle, and that is what makes it dangerous. The system did not error, did not warn, and did not present the answer as an approximation. It gave a specific name with complete confidence.

A wrong answer that looks exactly like a right answer is worse than an error, because nobody investigates it.

The general shape

The record stored a reference to something mutable and resolved it at read time.

That is fine for most data. If a customer changes their address, you want the current one.

It is wrong for anything that records what happened, because those records are statements about a moment. “This was approved under this policy by somebody holding this authority” is a claim about March, and resolving any part of it against today’s data produces a sentence that mixes two different times.

Anywhere a record is evidence, the values have to be captured at the moment of the event.

What changed

Three things, all snapshots taken at the moment the request was filed.

The people, not the role. The specific user identifiers eligible to approve at that instant.

The policy structure. How many stages, how many approvals each needs, what the amount thresholds were, and a version identifier so it can be traced to a definition.

The material facts of the request. The amount, the target, the line items, and the balance at the time. Not a pointer to a record that would keep changing.

request 8841
  filed_at         2026-03-12T09:41Z
  policy_version   14
  stage 1  needs 1 of [user 12, user 47]
  stage 2  needs 2 of [user 3, user 9, user 21]
  amount   142,500
  fingerprint  a91f...

Now every question about March is answered from the record itself, with no lookup against anything that has moved.

The objection, and the answer

The reasonable objection is that this duplicates data, and duplicated data goes stale.

That is exactly the intent. The record is supposed to be frozen. A snapshot that updates itself is not a snapshot, it is a view, and a view cannot answer a question about the past.

The instinct that duplication is always wrong comes from designing systems of current state, where it is correct. Systems of record are a different category and they obey different rules, and conflating the two is where this bug comes from.

The second benefit, which I did not anticipate

Policy changes stopped being frightening.

Before the snapshot, editing an approval policy would alter the meaning of every request currently in flight. Somebody adds a stage and requests halfway through their journey silently acquire a new requirement. Somebody removes an approver and requests waiting on that person become unresolvable.

The practical consequence was that policy changes were made rarely and nervously, usually late in the evening, after checking that nothing was pending.

With the snapshot, a policy edit applies to new requests only. Existing requests carry their own rules and are unaffected. The change became a two minute task done at any time of day.

That is worth naming as a general point: making a record immutable also makes the thing it references safe to change. The freedom was the unexpected part.

The fingerprint, which came from the same thinking

Once the request carried its own facts, the natural extension was to verify them before acting.

Requests sit for days. In that time the underlying data moves. A refund approved against a balance that has since been settled is not the request anybody agreed to.

So the material facts are fingerprinted at filing and rechecked at the moment of applying. If they differ, the system stops and says exactly what changed.

Without it, the approval is valid, the log is honest, and the effect is wrong, which is the worst combination available because every individual part of the system can defend itself.

What I took from it

Decide whether a record is state or evidence. State should reference and resolve. Evidence must capture. Getting this wrong produces answers that are confidently incorrect, which is the failure mode nobody catches.

Store what was true, not how to find out what is true. A pointer is a promise that the thing it points at will still mean the same thing later. For historical records that promise cannot be kept.

If somebody might ask about it a year from now, write down enough to answer without a join. The join will not be there, or it will be, and it will lie.

Published May 2026, updated July 2026.

Hiring someone to own work like this?

I am open to senior backend and full stack roles, remote and permanent.

Get in touch / Read the CV