All posts
EngineeringRefactoringBest Practices

Your Code Works. That's Not Enough.

8 min read

You open a file you haven't touched in three months. Maybe it's your own code, maybe it's someone else's. Either way, the feeling is the same: a slow creeping dread as you scroll through a 400-line function with variables named data2, tempResult, and my personal favorite, finalFinal.

It works. Nobody's disputing that. But there's one small feature to add, and suddenly you're pulling a thread that unravels a sweater you didn't knit and can't fully see.

That's usually the moment refactoring stops being a buzzword and starts feeling necessary.

What refactoring actually is (and isn't)

Refactoring is not rewriting. It's not the heroic two-week crusade where you throw everything out and start fresh. That's a rewrite, and it usually ends in tears, merge conflicts, and a Jira board that looks like modern art.

Refactoring is smaller than that. It's about improving the internal structure of code without changing what it does from the outside. Same behavior, better bones.

Think of it like renovating a house while people are still living in it. No demolishing walls. Just reinforcing things, rewiring what was held together with duct tape, removing the load-bearing pile of spaghetti someone installed in 2019.

Martin Fowler, who literally wrote the book on this, defines a refactoring as a "small behavior-preserving transformation." The word small is doing a lot of work there.

Code smells

Before fixing something, it helps to recognize it's broken. That's where code smells come in: patterns that suggest something might be off, even if nothing is technically crashing.

Some are obvious. A method that's 200 lines long is hard to miss. Others are subtler, like Feature Envy, where a method spends more time poking at another class's data than minding its own business. Or Primitive Obsession, representing everything with raw strings and integers instead of giving things proper types.

Here's one that shows up more than I'd like:

python
def process(type, data, flag, extra=None):
    if type == 1:
        if flag:
            # 40 lines of logic
        else:
            # 30 more lines
    elif type == 2:
        # completely different logic
    # and so on...

That function is doing five jobs and apologizing for none of them. A big if/elif chain dispatching to completely different behaviors based on a type flag is almost always a sign that polymorphism wants to exist, but nobody invited it.

The smell isn't the bug itself. It's the warning that the next painful change is on its way.

The thing nobody says out loud

Most developers know what refactoring is. Most teams claim they practice it. Almost nobody does it consistently.

And it's not laziness. Refactoring has no visible output. There's nothing to demo in a sprint review. The product looks identical before and after. From a business perspective, two days were spent and nothing changed, which is a hard sell when the backlog is screaming.

But the cost of skipping it compounds. Every feature added to messy code makes the next one harder. Slowly, then suddenly, most of the sprint goes to just understanding the codebase before writing anything. That's technical debt, and like financial debt, ignoring it doesn't make it disappear.

The teams that skip refactoring to move faster tend to be the ones who eventually freeze feature work for a quarter just to catch up. Speed now, wall later.

How it actually works in practice

There's no need for a dedicated "refactoring sprint." Those rarely work anyway. It's more of a habit than a project.

The Boy Scout Rule is probably the easiest entry point: leave the code a little cleaner than you found it. Already in a file fixing a bug? Rename that cryptic variable. Extract some nested logic into a well-named function. Nothing dramatic, just slightly better than before.

A few moves worth knowing:

  • Extract Method — if a block of code needs a comment above it to explain what it does, it probably deserves to be its own function with a name that explains it instead.
  • Replace Conditional with Polymorphism — that if type == X chain from earlier. Create a proper abstraction, let the objects handle their own behavior.
  • Introduce Parameter Object — passing five related arguments to every function? Wrap them. createUser(name, email, age, role, plan) becomes createUser(userDTO).

Small, targeted moves. Each one leaving things a bit more readable, a bit less scary to touch.

Why it matters at all

At the end of the day, refactoring is about respect for whoever reads the code next, which is often yourself six months later wondering what past-you was thinking.

The best codebases aren't impressive because of what they do. They're impressive because you can understand what they do in minutes, change things without fear, and add features without a ceremony.

That's not something that happens by accident. It's the result of small improvements made by people who cared enough to slow down a little.

The rough edges will always be there. The question is just whether they get slightly smoother each time, or keep piling up until someone inherits a codebase they genuinely dread opening.

Worth bookmarking: Refactoring.Guru has a solid free reference on code smells and techniques, with examples in most major languages.

All posts