Make the Change Easy, Then Make the Easy Change
Of all the habits that keep a codebase healthy, continuous refactoring matters most. When you refactor during a change, you make the change bigger. When you refactor before it, you make the change smaller. For every change you make, ask whether refactoring first would make it smaller and simpler. It usually would.
Continuous refactoring pays for the rest
Every codebase has a list of things that need attention: thin test coverage, stale comments, a module that picked up a second job, an abstraction that’s missing or one that shouldn’t exist. You will never get a quarter to fix all of it, and you shouldn’t wait for one.
Continuous refactoring is how that list gets worked down anyway. It’s the Boy Scout rule applied to code: leave it better than you found it. Every time you touch a file, make one thing better. Rename the variable that confused you. Rewrite the comment that lied. Pull the database call out of the function you’re testing so you can pass in a fake. Replace the third copy of a conditional with a lookup table.
None of those is worth a ticket on its own. Done constantly, over months, they decide whether the codebase gets cheaper or more expensive to change. Each small improvement lowers the cost of the next change, and the next change is always coming.
During makes it bigger, before makes it smaller
Say you need to add Slack as a notification channel. The if email ... else sms branch that picks a channel is copy-pasted in four places, and each copy is slightly different.
If you clean that up while adding Slack, you get one pull request that restructures four call sites and adds a new channel. The reviewer sees 300 changed lines and has to work out which dozen change behavior. If something breaks in production, reverting the feature pulls the cleanup out with it.
Do the cleanup first. Collapse the four copies into one Notifier.deliver(user, message) with no change in behavior, and get the tests green. Now adding Slack is one new branch in one place, and the feature diff is small enough to read in a minute.
Kent Beck’s version of this is one I quote at least once a month: “make the change easy (warning: this may be hard), then make the easy change.” The parenthetical matters. The refactor is often the hard part. You want the hard part to happen while behavior is held fixed, so the test suite can tell you the moment you get it wrong. That’s why good test coverage is a speed multiplier: it’s what lets you refactor without holding your breath.
Separate the refactor from the change
The practice that makes this work is keeping the refactor and the behavior change apart. At minimum, that means separate commits:
text
refactor: extract Notifier.deliver from four call sites
feat: add Slack notification channel
When the refactor is more than a few dozen lines, make it its own pull request and merge it first.
The two kinds of change need different reviews. A refactor reviewer is checking one thing: does everything behave exactly as it did before? The existing tests answer most of that. If you had to change a test’s assertions to get the refactor through, it changed behavior, and that part belongs with the feature. The feature reviewer can then spend all of their attention on the dozen lines that matter.
The split keeps paying off after the merge. git bisect lands on a small commit with one purpose instead of a mixed one. Reverting the feature leaves the cleanup in place, so the next attempt starts from the better code.
Refactor toward the change in front of you
Making the change easy simplifies more than the code. An easy change is easier to write, because you aren’t holding a tangle in your head while you type. It’s easier to review, test, and deploy. You end up simplifying the process of writing code along with the code itself, which is about as meta as engineering advice gets.
One caution. The refactor should serve the change you’re about to make. Restructuring code for a change you only imagine is future-proofing gone too far, and it adds complexity you’ll have to carry. The test is concrete: does this refactor make the diff I’m about to write smaller and simpler? If it does, do it first, in its own commit. If it doesn’t, leave it for the change that needs it.
Aim for simplicity every time. Make the change easy, ship that, and then make the easy change.