2026-08-25 · 5 min read
The duplication you should not remove
Fifty-two thousand duplicated lines across thirty-two apps, and most of it should stay. What the measurement changed about the plan.
The lab has thirty-two apps sharing a boilerplate. Shared backend files are not imported from a package — they are copied into each app by a sync tool. That arrangement invites an obvious reaction, and I had it: this is duplication, duplication is debt, extract it into packages.
Before extracting anything, I measured. Eleven files are byte-identical across all thirty-two apps. The AI engine is 847 lines with only two variants. Together that is roughly fifty-two thousand lines of duplication — a number that sounds like an obvious mandate.
Then check what is actually extractable
Convex requires any routed function to live in the app's own backend directory, so anything defining an endpoint cannot move regardless. That eliminated the auth, billing, profile, keys and workspace modules immediately — they are duplicated, but they are duplicated by the platform's rules, not by neglect.
That left six pure modules: five request-handling kits and the AI engine. Checking their imports took one command and changed the entire plan:
lib/rest.ts → import { httpAction } from "../_generated/server"
lib/mcp.ts → import { httpAction } from "../_generated/server"
lib/crud.ts → import type { Id } from "../_generated/dataModel"
lib/identity.ts → import { authComponent } from "../auth"
ai.ts → (no imports)Every kit depends on code generated per-app: that app's data model, that app's API surface, that app's server bindings. They are byte-identical *because* the sync works and the generated modules sit at the same relative path in every app. Lifting them into a package would mean threading all of that back in through generics or injection — turning five straightforward files into one abstract one, to remove duplication that is currently costing nothing.
Duplication is only debt when the copies can diverge. Copies that a tool regenerates from one source are not debt; they are a build artifact that happens to be checked in.
The one that mattered was mine
One file had no imports at all: the AI engine, documented as deliberately dependency-free because it is copied everywhere. And it contained a compact language table — which I had put there, the previous day, duplicating the registry that already existed in the faker.
That was the real find. Not fifty-two thousand lines of inherited duplication, but sixty-six lines of duplication I had created myself, in a file specifically designed to be copied thirty-two times.
Fix the risk, not the symptom
The obvious move — make the engine import the registry package — was available. Convex does bundle workspace packages; another app in the lab already does exactly that. But I could not deploy a Convex backend from where I was working, which meant shipping an unverified change to thirty-two applications on the strength of a typecheck.
So the engine keeps its table. What changed is that the table can no longer drift silently: a test reads the real file, parses its language rows, and holds them to the registry — every code present, no invented ones, fictional flags agreeing, aliases resolvable, and all thirty-two copies matching the source. Add a language to the registry without updating the engine and the suite fails with the language named and the fix stated.
I then checked the guard by adding a fake language and confirming it failed for the right reason, because a guard nobody has seen fail is not a guard — it is a hope with a green tick next to it.
The extraction that did happen was small: the language registry became its own package, the faker re-exports it so no caller moved, and the Python data file is generated from it. Everything else stayed put, with the reasons written down. The measurement was worth doing precisely because it argued against most of what it seemed to justify.