Tessera
The same five breaks, filed under two names
A reconciliation break has to survive a rerun — otherwise every note and assignment made on it vanishes the next morning. That means its identity has to be stable. Two call sites each computed that identity a different way, and the result was one queue holding the same disagreements twice.
- C#
- EF Core
- Blazor Server
- SQLite
- SQL Server
Identity has to survive the run that found it
A reconciliation runs every morning, and most of yesterday’s breaks are still there. If a rerun minted a fresh identity for each one, every note and assignment made yesterday would be orphaned and the queue would be useless by the second day. So a break’s identity is a hash of the reconciliation’s name, the matching key, and the kind of disagreement — computed the same way on every run, so the same disagreement always resolves to the same row.
That only works if “the reconciliation’s name” means one specific string, consistently. The step that runs the match declares exactly that: a required parameter documented as “break identities are derived from it”. One value, one place it is supposed to live.
Two callers, two readings of the same rule
The service that runs a reconciliation and updates the queue took the name as an argument rather than reading the step’s own parameter. Nothing enforced that a caller’s argument matched what the pipeline had actually declared — it was just another string, supplied wherever the service was called from.
The pipeline’s own display name and the reconcile step’s configured name were never the same string — a wholly ordinary thing for a person to type, since nothing suggested they needed to match. Every call path that used the pipeline’s display name produced an identity space the step itself had never claimed.
What that looked like in the queue
Running the same reconciliation from the designer and from the command line — an entirely normal thing to do while building and testing it — filed the same five disagreements under both names. Ten rows, five real breaks:
A break resolved through one path stayed open in the other, because to the queue they were not the same break — they hashed differently. The bug was invisible with a single caller, because a lone caller cannot disagree with itself. It took a second one, computing the same fact a second way, for the fork to become visible at all.
The fix removes the choice rather than validating it
The tempting fix is to check the argument against the step’s parameter and reject a mismatch. That still leaves two sources of truth and a check that has to run every time. The actual fix was to stop taking the name as an argument at all: the service now reads it directly from the reconcile step inside the definition. There is no longer a second place to type it, so there is nothing left to disagree.
A value with only one legitimate source should have exactly one line of code that produces it — not a contract that every caller is trusted to honour.
What holds regardless
- A stable identity needs exactly one computation, not several that are supposed to agree. The moment a second caller is free to supply its own answer, you have two systems that happen to match until they don’t.
- A bug with one caller and a bug with two callers can be the same bug. The first only looks correct because there is nothing yet for it to disagree with.
- Prefer deriving a fact from something already authoritative over asking every caller to pass it in correctly. Removing the parameter is a smaller surface than documenting how to use it.