Refactoring and technical debt
Why is everything slower?
A year after launch, Remy adds the Frost Beetle to Garden Defense. The Night Moth took two days last spring. The Frost Beetle takes a week. Every change breaks something else: the Halloween code has if (halloween) in 14 different files, and nobody remembers what speed2 means.
Nothing is broken for players yet. But the code has become slow and scary to change. That has a name: technical debt.
Borrowing time
The programmer Ward Cunningham came up with the debt idea in 1992. A shortcut is like borrowing time: we ship sooner today. But messy code charges interest, the extra time every later change costs. If a feature takes 4 days in clean code and 6 days in messy code, the 2 extra days are the interest.
Some debt is a fine choice, like a quick hack to get the Halloween event out on time. The mistake is never paying it back. Write it down as a GitHub Issue with a tech-debt label, so it doesn't get forgotten.
Quick check
In the debt picture, what is the "interest"?
- The extra time every later change costs because of the messy code
- The money the studio pays GitHub each month
- The time spent writing the first, quick version
Show the answer
The extra time every later change costs because of the messy code
The shortcut saves time once. The interest is paid again and again, every time someone has to work around the mess.
Code smells
- Giant filesOne file doing many jobs, like the old 5,000-line game.js from Stage 1.
- Copy-pasteThe same code in three places. Fix a bug in one, and the other two still have it.
- Magic numbersAn unexplained 1.8 in the code. Is it a speed? A price? Give it a name, or move it to balance.json.
- Unclear namesspeed2, data, doStuff. A name should say what a thing is for.
- Dead switchesEvent switches that stay in the code long after the event ended.
A code smell is a sign that something may be wrong underneath. In a live game, smells pile up with every event, every midnight hotfix, and every AI change nobody read closely.
Spot the smell
1function spd(p) {2 if (p.t === 'snail') return 1.8;3 if (p.t === 'beetle') return 1.8 * 1.5;4 if (p.t === 'moth') return 1.8 * 1.5;5 return 2;6}
Kai wants to make beetles a little faster for a balance test. What is the biggest problem with this code?
- The speeds are unexplained magic numbers buried in code, so tuning one pest means editing code and guessing what 1.8 and 1.5 mean
- The function is too short to be real game code
- Using return inside if statements makes the game run slowly
Show the answer
The speeds are unexplained magic numbers buried in code, so tuning one pest means editing code and guessing what 1.8 and 1.5 mean
Nobody can tell what 1.8 or 1.5 stand for, and the short names spd and p.t don't help. Named values in balance.json, like beetleSpeed, would let Kai tune one pest without touching code.
Refactoring: same game, better code
Refactoring means changing the structure of code without changing what it does. Martin Fowler, who wrote the classic book on it, describes it as a series of small steps that each keep the behaviour the same.
- Tests first. Tests are small programs that check the code gives the right answers (Tests: unit and integration, Stage 2). If nothing tests pest speeds yet, write tests for how they work today, before touching anything.
- One change at a time. Rename
spd, run the tests, commit. Move the numbers, run the tests, commit. - On a branch, merged through a pull request.
Quick check
A refactoring pull request that also adds the new Frost Beetle is fine, because it saves time.
- True
- False
Show the answer
False
A refactor should change no behaviour, so passing tests prove nothing broke. A new pest changes behaviour on purpose, and the reviewer can no longer tell which change is which. Two small pull requests are safer.
Refactoring with Claude
Claude Opus 5.5 is good at refactoring, if we set it up safely:
- Start in Plan mode, so Claude reads the code and suggests steps before it edits anything (Permissions, Stage 2).
- Ask for tests first, and for every test to pass after each step.
- Ask for small pull requests: one smell per PR.
- Review the diff, the list of changed lines (Pull requests and code review, Stage 2). Watch for deleted or weakened tests, and for changes to files nobody asked about.
- Then play the game.
Prompt pick
Remy wants Claude to clean up the Halloween checks spread across 14 files. Which prompt is safest?
- "On a branch, add tests for how Halloween works now, then move its checks into events.js in small commits, running the tests after each. Behaviour must not change."
- "Rewrite Garden Defense properly, it's a mess. Push to main when you're done."
- "Remove the Halloween code and add the Frost Beetle while you're in there."
Show the answer
"On a branch, add tests for how Halloween works now, then move its checks into events.js in small commits, running the tests after each. Behaviour must not change."
Tests first, small steps, a branch and a clear rule that behaviour stays the same. The second risks the version players use, and the third mixes removing a feature with adding one, so nobody can review it properly.
Budget it, and beware the rewrite
Pay debt back a little at a time. Our studio keeps about one day in five of every update for refactoring, starting with the code that changes most, because that's where the interest is highest.
Starting from scratch is tempting, and usually a trap. In 2000, the programmer Joel Spolsky called Netscape's choice to rewrite its browser from scratch "the single worst strategic mistake that any software company can make". Netscape went almost three years without a major release while rivals moved ahead. Old code holds fixes for real bugs, so replace it piece by piece instead.
Bolt says: Healthy code is easier to fix in a hurry. Next: when things break, with outages, attacks and backups.
Fact-checked 9 October 2026
- Martin Fowler: technical debt martinfowler.com
- Refactoring.com refactoring.com
- Joel Spolsky: things you should never do, part I joelonsoftware.com