Skip to main content
Handbook/Harden/Page 97 · Refactoring

Paying Down AI Debt

Share

Share this page

Pass it to someone who needs it.

Star on GitHub

Key takeaway: Delete more than you add

Your app works, but the code behind it has quietly bloated. Every feature you asked for, the agent answered with more structure than the job needed: an extra layer, an abstraction nobody calls twice. The codebase is harder to move through now, and you are not always sure which parts even run. This chapter gets you cleaning it up safely, by deleting more than you add.

10.8.1Improve without changing behavior

Refactoring is improving the structure of code without changing what it does. The behavior your users see stays identical; only the shape underneath gets clearer. Martin Fowler named the practice and gave it one core discipline: change structure and behavior in separate steps, never both in the same move.

This is why tests come first. The suite that pins your app's behavior down is what lets you reshape the code without fear: change the structure, run the tests, and if they stay green, only the shape moved.

Reshape the code, run the tests, and if they stay green only the structure moved.

10.8.2Delete more than you add

A human refactor usually means adding structure: extracting a shared function, adding a layer that was missing. With an agent the debt runs the other way. It already over-produced, so your cleanup is mostly subtraction, and every line you delete is a line you never have to debug.

Hunt for and delete:

  • dead code and branches that never run
  • options, parameters, and config nothing sets
  • duplicate helpers doing the same job
  • abstraction built for a second caller that never arrived

Rule of thumb: if you cannot say in one sentence why a layer exists, delete the layer, not the sentence.

10.8.3Book a session for upgrades, with no features in it

Refactoring pays down debt in code you wrote. There is a second pile you did not write and cannot refactor away: every library under your app, drifting further behind with every week you ignore it.

That work never happens by accident, because there is always a feature that feels more urgent. So book it like any other task: a session with no new features in it at all. Upgrade, read what changed, run the tests, ship. Once a month is plenty, and your tests are what make it safe enough to be boring.

Read the changelog before each upgrade, not after something breaks. Maintainers announce the breaking change there, in plain language, and skipping it is how a routine upgrade eats an afternoon.

Then notice what you are actually being handed. This is the only work in the whole book where somebody else already did it for you.

Security holes closed, code made faster, bundles made smaller, new capability added: all of it finished and merged by people you will never meet, sitting there waiting for one command.

Rule of thumb: an upgrade session with one feature in it is a feature session. The upgrades lose, every time.

10.8.4Simplify what the agent overbuilt

Deletion removes what is unused. Simplification shrinks what is used but overbuilt. An agent that adds abstraction and options nobody asked for is over-engineering, and refactoring is where you cut it back to the simplest thing that works.

The agent reaches for enterprise patterns on a problem that wants a plain function: a factory to build one object, an interface with a single implementation. Here it was asked only to join a first and last name.

// The agent's version: a factory, an interface, and a // config object, all to join two strings. interface NameFormatter { format(user: User): string; } class DefaultNameFormatter implements NameFormatter { constructor(private config: FormatConfig = {}) {} format(user: User): string { const sep = this.config.separator ?? " "; return [user.first, user.last].filter(Boolean).join(sep); } } export function createNameFormatter() { return new DefaultNameFormatter(); }
// Same behavior. Nothing else set the separator, so // it is gone too. One function does the whole job. export function fullName(user: User): string { return [user.first, user.last].filter(Boolean).join(" "); }

Same inputs, same output, a quarter of the code and one obvious place to change it. Have your agent run that same pass on your worst file:

Ready prompt
Act as a senior engineer refactoring for simplicity, not adding features. Behavior must not change. Read my rules file first, the conventions, the architecture map, and my quality bar, and keep the smaller version inside them. State in one sentence what this code does, so we have a fixed target to preserve. Then find everything safe to remove: - dead code and branches that never run - options, params, and config nothing sets - duplicate helpers that do the same job - factories, interfaces, or strategies with a single implementation Propose the smallest version with identical behavior. Show a before/after diff and say why each removal was safe. Add nothing new: no features, no comments, no abstraction. If tests exist, keep them green; if not, tell me what to test before I delete anything. Note anything notable you removed in my changelog. If you need the full reasoning behind this step, read https://zalt.me/guides/vibe-coding/harden/refactoring The code that feels overbuilt:

Do this now: open the file in your app you least want to touch, run the prompt on it, and let your agent show you a smaller version that keeps every test green.

Mahmoud Zalt

Mahmoud Zalt

Software engineer, 16+ yrs · built Sistava.com in 3 months, idea to production, using these methods

Resources
Star on GitHubContribute
Donate

Support my work

A small tip keeps the free work coming.

© 2026 Mahmoud Zalt. Free to read, not to republish.
Copyright & license