Case study
When one number has four owners, three of them are wrong
A user could ask two parts of the same system the same question in one afternoon and get two different answers. Both were defensible. That is the worst kind of bug, because nobody can tell you which one to trust.
- Role
- Sole engineer
- Period
- 2024
- Stack
- Node.js, PostgreSQL
The problem
Four different parts of the system each worked out what somebody owed. Billing had one figure, the customer facing portal had another, the reporting layer had a third, and the collections logic had a fourth.
None of them were careless. Each had been written by somebody solving a real problem, each handled a slightly different set of edge cases, and each was defensible in isolation. They disagreed on partial payments made mid period, on credit carried forward, and on adjustments applied after an invoice was raised.
The visible symptom was staff losing confidence in the system. Somebody would quote a balance to a customer standing in front of them, the customer would check the portal, and the two would not match. Once that happens twice, people stop trusting every number on every screen, including the ones that were always right.
What I tried first, and why it was wrong
I made the four agree.
Shared helpers for the tricky parts. A reconciliation of the edge cases. Tests that ran all four against the same fixtures and asserted equality. It took a couple of weeks and it worked.
It held for about a month.
The next feature needed a figure in a slightly different context, and the fastest path was a fifth calculation. Not out of laziness. The existing four were still four, so adding a fifth was consistent with the codebase as it stood.
That was the lesson. Agreement is not a fix, it is a truce, and truces expire. The design still permitted an opinion, so opinions kept appearing. Tests pin the behaviour that exists today. They do not prevent tomorrow’s implementation, because the architecture still says implementing one is a normal thing to do.
The decision
One component owns the calculation. Nothing else is permitted to compute it. Everything else asks.
The definition became explicit and singular: the total is what is outstanding for the current period, plus anything unpaid from earlier periods in the same financial year, plus anything carried forward from before it. Written down once, in one place, in the language the business actually uses.
Every other subsystem became a reader. Reporting reads it. The portal reads it. Collections read it. Anything that produces a charge writes through it rather than around it.
What that trade costs
Worth being honest about, because the decision is not free.
One component becomes a bottleneck for change. Everything that touches money now queues behind changes to one file, and that file has more reasons to change than any other.
A bug in it is a bug everywhere at once, with no partial blast radius.
And it is slower to build than letting each surface compute what it needs, which is exactly why the original four existed.
I took that trade knowingly, on one argument: a single figure that is wrong consistently is recoverable, and four figures that disagree are not. With one owner, a bug is found once, fixed once, and every surface is correct the moment it is fixed. With four, you are not fixing a bug, you are conducting an investigation into which of four answers was ever right.
How I proved it
Not with unit tests, or not only with them. Unit tests confirm the calculation matches my understanding, and my understanding was the thing in question.
Every summary was reconciled against its own detail across a full year of live data, at every level of aggregation. If a headline said one thing, the rows underneath it had to sum to exactly that. Any discrepancy was a real defect and was investigated individually rather than tolerated as rounding.
Then I walked it in the browser as the person who would actually be blamed if it were wrong, rather than through the API where everything always looks fine.
The migration
Nothing was switched over in one step. The new component was built alongside, both figures were computed for every request, and the difference was logged without changing any behaviour.
That log was the most valuable artefact in the whole project. Every difference had an explanation, and finding those explanations is how I learned what the original four were actually doing, including two edge cases that turned out to be correct behaviour nobody had documented.
Only once the differences were all understood did the reads move across, one surface at a time. The old calculations were deleted last, months later, when nothing referenced them.
What I would change
I would have stopped sooner. I spent two weeks making four things agree before questioning whether four things should exist. The signal was there on day two, when the shared helper needed a parameter to behave differently per caller. A helper that needs to know who is calling it is telling you the callers should not exist.
Questions people actually ask
- Why not just add tests to make the four agree?
- Tests pin behaviour that exists. They do not stop a fifth implementation being written next quarter, because the design still permitted one. The fix had to remove the permission, not the disagreement.
- Is a single owner not a bottleneck?
- Yes, and that is the trade I took knowingly. One component becomes a dependency for changes and a bug in it is a bug everywhere. A single figure that is wrong consistently is recoverable. Four figures that disagree are not.
- How do you migrate to this without a rewrite?
- Compute both, log the difference, change nothing. Once the difference is understood and explained, switch the reads one surface at a time, and only then delete the old calculations.