The Drift Log · Audit, then actually repair
Consolidating Duplicate Utility Functions Across Monorepos
24 September 2026 · 4 min read · 811 words · established

Turn dead-end duplicate code audits into safe monorepo consolidation using invariant testing, union contracts, and automated AST codemods.
In a monorepo with thirty packages, five different engineers will write five distinct implementations of slugify, truncateString, or safeJsonParse. They do not do this because they enjoy writing string manipulation logic. They do it because searching through twelve sibling packages and five shared directories takes longer than writing ten lines of TypeScript in a local utils/ folder.
Two years later, those five implementations have diverged. One handles unicode combining marks; four do not. One swallows parse exceptions silently; another throws a custom domain error; a third returns undefined. You now have silent divergence across production boundaries, duplicated maintenance overhead, and no single place to fix a security or performance bug.
Running a static analysis tool or a grep script to produce a list of identical functions feels productive. It is not. An audit that produces only a list of duplicates is just a technical debt report that nobody has time to read. The value is created only when you have a mechanical path to merge those implementations into a single verified package and update the call sites.
The Cost of the Read-Only Audit
Most attempts at monorepo refactoring stall at the inventory stage. An engineer runs a clone-detection tool, finds 400 instances of duplicated utility logic, logs a ticket titled "Consolidate Shared Helpers", and moves on to sprint work.
That ticket dies in the backlog for three reasons:
- Diffidence about edge cases. Nobody wants to replace a local utility in the billing package with one from the auth package if they are not certain the behavior matches under all inputs.
- Blast radius anxiety. Touching twenty packages in a single pull request creates merge conflicts with every active branch and breaks branch-based CI runs.
- Lack of an authoritative home. If the monorepo has no strictly governed
packages/primitivesor shared core, moving code requires an architectural debate about package hierarchy.
If an audit does not terminate in automated pull requests, it is pure operational friction. As we explore in our guide on closing the loop from repository audit to merged repair, the audit is merely the discovery phase of a repair pipeline.
Constructing a Safe Consolidation Path
To eliminate duplicate utility functions without breaking downstream consumers, you need a repeatable consolidation loop:
[Scan AST for Duplicate Logic]
│
▼
[Extract Union Contract & Write Test Suite]
│
▼
[Promote to Canonical Shared Package]
│
▼
[Automate Call-Site Rewrites via Codemod]
1. Extract the Union Contract
When two functions share 90% of their structure but differ in how they handle null or empty strings, you cannot simply delete one and import the other. You must define a single contract that satisfies the union of both caller expectations—often by making edge-case handling explicit via option parameters.
2. Write the Invariant Tests First
Before touching call sites, extract the target implementation into your shared workspace package and construct tests that assert every observed variant's input/output expectations. If package A expected null on invalid inputs and package B expected an empty string, document that divergence in tests before refactoring either.
3. Codemod the Call Sites Incrementally
Do not manually edit 60 files across 15 packages. Use AST-based transforms (such as jscodeshift or ts-morph) to swap local imports for the canonical package path. This allows you to land the migration across multiple smaller pull requests rather than one massive, unreviewable diff.
For teams planning broader repository restructuring, harvesting shared primitives before monorepo rewrites prevents these utilities from getting lost during larger architectural shifts.
Where Consolidation Breaks: Silent Divergence
The hardest part of any code reuse audit is not finding identical code. It is finding near-identical code that diverged intentionally six months ago to satisfy an undocumented constraint.
Consider a retry utility:
// apps/sync/src/retry.ts
export async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
// Uses exponential backoff with jitter
}
// apps/webhooks/src/retry.ts
export async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
// Uses linear backoff because downstream rate limits reset every second
}
If you blindly collapse these into a single helper without inspecting the call sites, you break the webhooks service under load.
When consolidating, never assume identical function names or similar signatures imply identical runtime requirements. If two utilities have divergent requirements that cannot be unified cleanly into parameterized shared primitives, leave them separate, rename them to reflect their specific operational constraints (e.g., withLinearRetry vs withExponentialRetry), and document why they exist.
What to Do on Monday
Do not schedule a month-long technical debt repair epic. Pick one low-risk, ubiquitous utility category to test your consolidation pipeline:
- Search your repository for string manipulation or date formatting helpers (e.g.,
formatDate,clampString,parseQueryString). - Identify every duplicate implementation across packages.
- Check our blueprint on cataloguing in-tree utilities to prevent duplicate work to establish a clean internal baseline.
- Promote the most robust variant to your shared primitives package, write comprehensive boundary tests, and publish it internally.
- Write a simple codemod to update calls in two non-critical packages, verify CI, and merge.
Once the pipeline works for one utility, you can run it against the rest of the workspace systematically. The goal is not an empty duplicate list; it is a mechanical process for turning repeated code into maintained, test-backed assets.
Keep reading
Next in the log
- Harvesting Shared Primitives Before Monorepo Rewrites
Total rewrites discard years of operational scar tissue. Extract framework-independent domain logic into verified primitives before tearing down legacy services.
- Convert Audit Findings into a Prioritised Repair Backlog
Turn audit reports into structured, call-graph-prioritised repair tickets paced strictly to match sprint velocity and avoid backlog paralysis.
- Prioritising Repairs by Load‑Bearing Impact
Prioritize audit findings by weighting code paths that power critical product features.
The Strategic Master Library · written and reviewed under the house's own epistemic rules: nothing claimed that we cannot show.