---
title: "The Duplicated-Derivation Bug: One Fragile Expression in Four Queries"
canonical: https://dxdev.com/blog/duplicated-derivation-is-a-correctness-hazard/
datePublished: 2026-02-26
---
A tournament feature kept linking the wrong sport. You'd wire a tournament up to its parent and the link would either vanish, point somewhere it shouldn't, or quietly refuse to be created because the system swore one already existed. The behavior wasn't random. It depended on which path you came in through. Link did one thing, unlink did another, and the duplicate-check that was supposed to keep them honest had its own opinion entirely.

The cause turned out to be almost embarrassingly small. Four separate SQL queries each re-derived the tournament's struct ID inline, with a slightly different brittle expression in each spot. Nobody wrote a wrong query. They wrote four queries that each, alone, looked right, and together couldn't agree on what the key even was.

## The shape of the bug

This is a legacy classic-ASP app. The request context object carries the current page state, and somewhere along the line a convention emerged that a tournament's struct ID is "usually the username, unless it's a tournament with an org name, in which case it's the org name." That rule lived in exactly nobody's head as a single sentence. It lived as a copy-pasted expression.

At one call site, the WHERE clause built its key like this:

```
escape((ctx.isTournament && ctx.orgName) || ctx.username)
```

At another, the same conceptual lookup was just:

```
escape(ctx.username)
```

Same intent on every one of them: "what's this tournament's struct ID?" Four inline implementations of that intent, one each in the header query, the link path, the unlink path, and the duplicate-check.

Here's why that's lethal and not just untidy. The full ternary resolves to `orgName` in some cases and `username` in others. The bare version always resolves to `username`. So a link could be written under the org-name key while the existence-check looked under the username key. From the dup-check's point of view, the link it just wrote didn't exist. From the unlink's point of view, there was nothing to remove. Every path was internally consistent and the set of them was incoherent.

That's the part worth sitting with. None of these queries is buggy in isolation. You can read any one of them, decide it's reasonable, and move on. The bug only exists in the gap between them, which is why it survives code review. Review looks at lines, and the defect isn't on any line. It's in the disagreement.

## The tell in the diff

When I finally fixed it, the diff had a signature I now treat as a smell on sight. The same `where:"entityID = '" + escape(...) + "'"` line changed in four places at once, and each one started from a different argument to the identical helper.

That pattern, "N call sites, same skeleton, divergent guts," is what derivation logic looks like when it should have been a function from day one and never got promoted. Each call site is a fossil of whatever the author believed the rule was on the afternoon they wrote it. Put four of those fossils next to each other and you can watch the rule drift across the codebase like a game of telephone played in source control.

## The fix

The fix is the boring one, and that's the point. Extract a single resolver, call it something like `getTournamentStructID()`, make it the one place that knows the org-name-vs-username rule, and replace every inline derivation with a call to it. The header query in the detail page routes through it. The three ajax operations on the tournament (link, unlink, and the duplicate-check) all route through it.

After that, link, unlink, and dup-check cannot disagree about the key, because there's no longer four answers to disagree with. There's one. If the rule is wrong, it's wrong everywhere at once, which sounds bad until you realize "wrong in exactly one place I can find and fix" is a strictly better failure mode than "wrong in one of four places, and you get to guess which."

The whole change was one commit, and the resolver was maybe a handful of lines:

```
function getTournamentStructID(ctx) {
  return escape((ctx.isTournament && ctx.orgName) || ctx.username);
}
```

One place that knows the rule. The bug it killed had three distinct symptoms.

## Why I'm calling this a correctness hazard, not a style one

Every "don't repeat yourself" argument I'd internalized framed duplication as a maintenance tax. You copy a thing, now you have two places to update, you'll forget one, future-you pays. True, but it undersells the actual danger. That framing makes DRY sound like flossing: virtuous, easy to skip, no immediate consequence.

The consequence here was immediate. The duplication didn't make the code harder to maintain in some abstract future. It made the code wrong in the present, on a feature customers were using, in a way that produced different results depending on which door you walked through.

The reframe I took away: when the duplicated thing is the answer to "what is X?", copies of that answer are not redundant data, they're competing sources of truth. And the moment you have more than one source of truth for the same value, the answer you get depends on which call path you came in through.

## The rule I now apply

So here's the thing I check for now, and it's narrower than "avoid duplication" because blanket DRY advice gets you to over-abstract everything and that's its own mess.

If a value is computed inline, and the same value gets computed inline somewhere else, and the two derivations could ever diverge, that's a resolver waiting to be extracted. Not for elegance. Because the alternative is that the two copies disagree exactly once, in production, on the path you didn't test, and you spend an afternoon convinced your database is corrupt when really your code just can't agree with itself about what row it's pointing at.

Three signs you're looking at one of these:

- The same skeleton appears in multiple queries or branches with a slightly different argument or guard in each.
- The thing being computed answers a question ("which ID?", "is this allowed?", "what's the active record?") rather than just transforming data.
- Different operations on the same entity (create, read, delete, check-exists) each build the key or predicate themselves instead of calling a shared thing.

When all three line up, extract the resolver before you go hunting for the bug, because the resolver IS the fix. You don't have to find which of the four copies is wrong. You make there be one copy, and the disagreement that was the bug stops being expressible.

## Related

- [GetAccountFilter and the Case for an Explicit Context You Can Override](explicit-context-object-you-can-override-default-arg): same legacy seam, one implicit ambient value that multiple call sites each resolved differently
- [The Ambient-Global Bug: When "Current Account" Is a Mutable Global Your Whole App Reads](the-ambient-global-bug-mutable-current-account): the ambient-global pattern that makes duplicated derivation possible at scale
- [N Drifting Copies Share One Bug: The Legacy Consolidation Thesis](n-drifting-copies-share-one-bug-legacy-consolidation): N copies of the same logic drift until they disagree; the resolver is the fix across the whole codebase
- [The ambient-global bug: when "current account" is a mutable global 100 call sites read](ambient-global-current-account-context-switch): a parallel cut of the same ambient-context divergence pattern
- [A 47,490-line JavaScript file and what it tells you about a profitable legacy app](47000-line-js-file-profitable-legacy-app): why large legacy files accumulate duplicated derivations and what the file size means
