"One number, one source": the balance-engine pattern that ended it
The short version
My app showed two people two different answers to the same question, and my own records had a third. The cause was not a hard bug. Every screen was calculating the balance separately, so every screen could drift.
The fix is a rule, not a patch: exactly one piece of code is allowed to do the arithmetic, and everything else asks it for the answer. If your app shows a number people trust, that number needs one home.
Copy these prompts
Paste them into Claude Code (or any AI that can see your project), in this order. Each one answers a different question, and the first is usually the one that matters.
1. Find every place your app works out the same number
Show me every place in my app that calculates the number my users actually trust: a balance, a total, a score. List each one separately and tell me how many different places compute it. If it is more than one, show me where two of them would disagree today.
2. Make exactly one of them the source
Move that calculation into a single function and show me how every screen would call it instead of doing its own version. Tell me which screens I would have to change, and what would break while I am changing them.
3. Check what happens when somebody joins halfway through
Check whether a person who joins my app or a group partway through gets counted in things that happened before they arrived. Show me where that decision is made, and write a test that proves a new member is not charged for history.
My QA fleet caught it: phone A said "they owe you $43.33", phone B said "you owe $57.27". Same pair of friends, $14 apart, and the server had a third number. No single bug caused it. The architecture did: every screen recomputed balances its own way, custom splits silently fell back to equal splits, and new members were getting charged for expenses from before they joined.
The pattern
One number, one source. Any number your users trust (a balance, a total, a score) is computed in exactly ONE engine. Nothing else in the codebase is allowed to do arithmetic on it.
The playbook
- Create a single pure engine. Mine is a
BalanceEngineclass: rows in → balances out. No I/O, no caching, no framework imports. Pure = trivially testable. - Ban arithmetic everywhere else. Screens render balances; they never recompute, never
cache, never sum rows themselves. Enforce it in review: any
+/-on money outside the engine is a rejected PR. - Derive, never store. Balances are computed from expense/settlement rows on read. A stored balance is a cache, and caches of money go stale (my reviewer agent later caught exactly that: a cached balance that survived a settle-up).
- Feed the engine from one repository. One source of rows → one engine → one provider that screens subscribe to. My invariant doc literally says: "I1: One number, one source."
- Make fallbacks loud. The killer sub-bug: custom splits silently degrading to equal splits. If your engine can't honor an input, throw or flag. Never "do something reasonable" with money.
- Time-scope membership. New members must not owe for pre-join expenses. Every share calculation filters participants by join date.
- Test the engine like it's your whole company. Cross-device scenarios, settle-then-check, join-mid-trip, multi-currency. My balance math now has hundreds of tests. It's the one module where that's not overkill.
- Reconcile in QA, not in prod. Run a job that computes balances two independent ways (engine vs. raw SQL) and alerts on any mismatch. That's how you find the third number before a user does.
Steal this for your app
- Find YOUR "one number": the figure users would screenshot in an argument (balance, streak, invoice total, calories). Give it a single home this week.
- Grep for duplicate computation: if the same domain math appears in 2+ files, you already have this bug, it just hasn't disagreed loudly yet.
- Pure function + heavy tests beats clever caching. Recompute is cheap; wrong is expensive.
Run this on your codebase
Paste this into Claude Code in your repo:
Audit this repo for "one number, one source" violations.
Identify the trust-critical numbers users see (balances, totals, scores) and find every place each is computed.
Grep for duplicate domain math: the same sum, split, or rounding logic in more than one file, screen, or query.
Flag stored or cached values that duplicate what could be derived from source rows on read.
Flag silent fallbacks where invalid input degrades to a default instead of throwing, like custom splits becoming equal splits.
Check that members or time-scoped records cannot be charged for events from before they existed.
Propose one pure engine per number, with screens only rendering its output.
Report findings as a checklist before changing anything.
CENTS: remove floating-point money before it removes trust
The single-source engine still fails if the source stores 10.10 as a binary float. Audit every
money field and convert it to an integer minor unit, such as cents. Parse at the boundary, calculate
with integers, and format only for display. For a three-way split, allocate the remainder with a
stable rule so the shares always add back to the original amount. Then add boundary and
property-based tests for zero, negative adjustments, very large values, and awkward divisions.
The migration sequence matters: add the integer column, backfill and reconcile it against the old value, switch all writers, switch all readers, then remove the float. Never run two independent money representations without a reconciliation check.
NORMAL: one fact, one canonical home
Normalization is the structural version of "one number, one source." Store each changing fact once and reference it by ID. Before copying a field into another table, classify the copy explicitly:
- A snapshot is intentionally historical and should not change.
- A cache may be copied for speed, but needs an owner, invalidation path, and repair job.
- An accidental duplicate is a future disagreement and should be removed.
Test propagation, deletion, retry, and repair sequences. If a user changes a name, status, or price, every current view should agree without a manual cleanup query.
Download the runnable pack
Use this pack to dry-run the audit locally, trigger the same workflow in n8n, and keep the checks in GitHub Actions. The default mode is read-only and returns a deterministic report before any change.