The Drift Log · Audit, then actually repair
Refactoring Reusable Logic Out of Legacy Codebases
25 September 2026 · 3 min read · 755 words · established

Turn static audit findings into merged pull requests by carving pure calculations out of side-effect-heavy legacy code.
Most repository audits end in a PDF that nobody opens twice.
The scanner runs, finds cyclomatic complexity scores in the triple digits, flags fifty instances of duplicated business logic across legacy services, and dumps the findings into an issue tracker. Three weeks later, the tickets are stale. The audit identified technical debt, but it created diagnostic debt: a list of known failures that the team lacks the safety margin to touch.
Refactoring fails here because teams attempt to clean legacy code in place. When a 600-line controller handles database I/O, authentication, session state, and domain math simultaneously, altering it inside its execution path carries unacceptable regression risk. The safest way to turn audit findings into merged code is not to rewrite the module, but to carve out its pure logic and leave the surrounding wiring behind.
Isolating the Pure Core
Every sprawling legacy method contains a smaller, deterministic calculation smothered by side effects.
Consider a typical finding from a repository audit: an unmaintainable billing reconciliation routine. The code reads from three database tables, checks an environment variable, makes an external gateway call, mutates an in-memory session object, and calculates an adjusted proration fee.
// Legacy: calculation tangled with network and state
async function reconcileSubscription(userId: string, targetTier: Tier) {
const user = await db.users.findById(userId);
const invoice = await billingGateway.getLatestInvoice(user.stripeId);
// Mixed-in business rule targeted by the audit
let proration = 0;
const daysRemaining = (invoice.periodEnd - Date.now()) / (1000 * 60 * 60 * 24);
if (daysRemaining > 0 && invoice.amount > 0) {
const dailyRate = invoice.amount / 30;
proration = Math.round(dailyRate * daysRemaining * targetTier.multiplier);
}
user.tier = targetTier.id;
await db.users.save(user);
return proration;
}
The repair fails if you try to clean up the entire database lifecycle or mock the external gateway. You do not need to rewrite reconcileSubscription to fix the code health of the billing logic.
Instead, slice the proration logic out into a standalone, pure function:
// Extracted: pure domain logic, zero dependencies
export interface ProrationInput {
periodEndMs: number;
nowMs: number;
invoiceAmountCents: number;
tierMultiplier: number;
}
export function calculateProration(input: ProrationInput): number {
if (input.invoiceAmountCents <= 0 || input.tierMultiplier <= 0) {
return 0;
}
const daysRemaining = (input.periodEndMs - input.nowMs) / (1000 * 60 * 60 * 24);
if (daysRemaining <= 0) {
return 0;
}
const dailyRate = input.invoiceAmountCents / 30;
return Math.round(dailyRate * daysRemaining * input.tierMultiplier);
}
The controller now delegates to the extracted function. The side effects remain messy, but the core business rule is isolated, explicit, and covered by deterministic unit tests that run in milliseconds.
Why Extraction Beats In-Place Rewrites
In-place refactoring forces you to prove that the entire component still behaves identically across every environment. That requires comprehensive integration tests that legacy codebases rarely have.
Extraction changes the unit of risk. When you slice out pure logic:
- State mutation stops hiding. You must declare every input explicitly in the parameter signature. If the logic relied on an implicit global variable or a mutated object property, the extraction forces that dependency into the open.
- Test coverage is cheap. You do not need database mocks, test containers, or network stubs. You write table-driven unit tests that hammer boundary conditions, zero values, and negative inputs.
- The code becomes portable. Once logic is decoupled from runtime dependencies, it can move to a shared utility directory or a central package.
This is the practical bridge between static findings and merged pull requests. As detailed in our guide on closing the loop from repository audit to merged repair, audits only deliver value when findings convert directly into verifiable, low-risk patches.
Handling the Hard Parts
Not all legacy code yields cleanly to extraction. Two failure modes occur regularly:
- Hidden mutations: The calculation modifies a nested property on an argument passed to it, and downstream code relies on that mutation. You must clone inputs or refactor the caller to accept the function's explicit return value instead of relying on side effects.
- Tightly coupled temporal dependencies: The code performs I/O, does partial math, performs more I/O based on the interim result, and finishes the math.
When you encounter temporal coupling, do not attempt to solve the whole chain in one pull request. Extract only the sub-calculations that can be made pure without changing the sequence of side effects. If you are cataloguing these repairs, use the methods in converting audit findings into a prioritised repair backlog to sequence them before undertaking broader architecture changes or harvesting shared primitives.
What to Do on Monday
Pick one file flagged for high complexity or duplication in your last static analysis run.
- Find one calculation. Look for a block of math, string formatting, or collection transformation longer than ten lines that does not directly invoke
await,fetch, or a database query. - Cut it out. Move it to a separate file as an exported, pure function. Pass all required values in as arguments; return a single structured output.
- Cover the edges. Write five unit tests for the extracted function: happy path, zero values, negative numbers, empty arrays, and invalid dates.
- Replace the call site. Import the new function into the legacy method and replace the inline block with a single function call.
Open the pull request. It will be small, obviously correct, and easy for your team to review and merge. Repeat this pattern across your backlog, and the audit report stops being an indictment—it becomes a mechanical repair checklist.
Keep reading
Next in the log
- Consolidating Duplicate Utility Functions Across Monorepos
Turn dead-end duplicate code audits into safe monorepo consolidation using invariant testing, union contracts, and automated AST codemods.
- 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.
The Strategic Master Library · written and reviewed under the house's own epistemic rules: nothing claimed that we cannot show.