Chapter 10: Refactoring is Returning
“We shall not cease from exploration, and the end of all our exploring will be to arrive where we started and know the place for the first time.”
— T.S. Eliot
For two years, we bolted things onto the billing system.
Each January, the business teams would announce new pricing structures. Base rates changed. Usage thresholds shifted. Overage calculations evolved. And each year, my team would carve another path through the code, another bespoke handler for the new rules. The first year’s changes lived in one corner. The second year’s lived in another. They shared some logic but not enough, duplicated some structures but not consistently. The system worked, technically. But every time I opened those files, I felt the weight of decisions made under deadline pressure, each one reasonable in isolation, incoherent as a whole.
When the third year’s requirements arrived, I looked at the code and felt something shift. I could see where we were headed: three parallel paths, then four, then five, each one a little different, each one a little harder to reason about. The pattern was obvious now. We weren’t building a system. We were building a museum of yearly compromises.
So instead of bolting on a third path, we stopped. We refactored.
We turned each year’s changes into a plugin, a self-contained set of instructions. We built a generic engine that could load the right plugin based on contract dates and apply the changes dynamically. The work was harder than just adding another path would have been. It took longer. But when it was done, the system had become what it always wanted to be. The complexity hadn’t disappeared, but it had found its home. Reasoning about plan changes became straightforward. Bugs dropped. We built tooling for customer support to see exactly what had changed and why.
That’s what refactoring feels like when it works. The code reveals its true shape, and you realize you’ve been looking at it sideways the whole time.
The Necessary Mess
The first draft is never the final form. It can’t be.
You can’t draw a map of territory you haven’t walked. The messy first version is the walking. It’s exploration, not failure. You’re learning the domain, discovering the edges, finding out which assumptions hold and which ones crumble. The code you write during exploration reflects incomplete understanding, because that’s all you have.
This is uncomfortable for engineers who prize correctness. We want to get it right the first time. We want to architect the solution, implement it cleanly, and move on. But domains don’t reveal themselves on demand. They reveal themselves through contact, through building the wrong thing and noticing why it’s wrong, through living with the code long enough to feel where it binds.
The billing system’s first two years weren’t mistakes. They were how we learned what billing actually meant for our customers. Without those bespoke paths, we never would have seen the pattern that unified them. The mess was the price of insight.
The code you wrote then was the best code you could write then. The code you write now reflects what you know now. Aligning the two is reconciliation.
The Signals
How do you know it’s time?
Your body knows before your metrics do.
There’s a particular friction that builds up around code that needs refactoring. Every change takes longer than it should. You find yourself re-learning the same context every time you open the file. Names that once made sense have drifted from their meaning. You’re passing parameters through three layers of functions just to use them at the bottom.
The clearest signal is dread. When you flinch at the thought of touching a part of the codebase, that’s not laziness. That’s information. The code is telling you something: its structure no longer matches reality.
Another signal: you start adding conditionals to avoid understanding. Instead of refactoring the function to handle the new case cleanly, you wrap it in an if statement. Instead of rethinking the data model, you add a special flag. These are shortcuts around comprehension, and they compound. Each one makes the next change harder, the code more brittle, the dread deeper.
When the code starts charging interest on every change you make, it’s time.
The Resistance
Not all refactors feel like returning to a place you understand. Some feel like returning to a place you’ve been avoiding.
We avoided refactoring the automation engine for years.
It was the heart of the system, the core that everything else depended on. The founding engineer had built it in the early days, and it carried his fingerprints everywhere. Over time, other engineers had added to it, extended it, patched it. The code worked. Features shipped. Customers used the product.
But velocity was slowing. Every change had ripple effects we couldn’t predict. A fix in one workflow would break three others. The test suite took hours to run because everything was entangled. New engineers took months to become productive in that part of the codebase. The people who understood it best were afraid to touch it.
We kept saying we’d refactor it after the next release. The next release came, and we said it again. The mess grew. The fear grew. The codebase became a place where ambition went to die.
When we finally bit the bullet, it wasn’t because we wanted to. It was because we no longer had a choice. Velocity had crawled to a standstill. We couldn’t ship the features the business needed without addressing the foundation they sat on.
The first few weeks were terrifying. We’d torn the engine apart but hadn’t yet put it back together. Half the tests were red. The old code was gone, the new code wasn’t working yet. Every standup felt like a confession: still in the middle of it, still don’t know when we’ll be out. There were days I wondered if we’d made a terrible mistake, if we should have just kept bolting things on forever.
Then the pieces started fitting. The new structure emerged. Tests turned green. And when it was done, the system we’d been afraid of for years became a system we could reason about, extend, trust.
Looking back, we should have pushed for it years earlier. The refactor took months, but the time we lost to fear and friction over those years dwarfed it. We paid in bugs, in slow deployments, in engineer burnout, in features we couldn’t build. The resistance felt like prudence at the time. It was debt accumulating in the dark.
The Relief
Sometimes refactoring means deleting more than you keep.
We had a data layer built on a NoSQL store. It had seemed like the right choice at the time, flexible and fast for our early needs. But as the product matured, we kept fighting it. Queries that should have been simple required elaborate workarounds. Data consistency was a constant battle. The code to manage it had grown into a sprawling mess of edge cases and manual joins.
The migration to SQL took months. We rewrote the data access layer from scratch, built scripts to move everything over, tested exhaustively. The tests from Chapter 9 made this possible. Without that safety net, we never would have had the courage to delete so much. I remember the moment we finally cut over: delete the old code, run the suite, watch it go green. I slept well that night.
The new system was forty percent the size of the old one. Forty percent. We hadn’t just changed databases. We had stopped using the wrong tool for the job. The code got quieter. The queries got simpler. Problems that had haunted us for years simply vanished.
Deletion felt like relief, not loss. Like taking off a heavy backpack you’d forgotten you were carrying.
Refactoring sometimes means changing your dependencies, rethinking your foundations, admitting that a choice made years ago no longer serves. The code you delete is not wasted effort. It was the exploration that taught you what you actually need.
The Practice
Not every refactor is a months-long migration. Most refactoring is smaller than that. A rename here. An extraction there. A bit of cleanup while you’re already in the file.
Call it micro-returning. You came to fix a bug. Before you leave, rename that misleading variable. Extract that duplicated logic. Delete the dead code you noticed on your way through. Each small act aligns the code slightly more with your current understanding. These aren’t chores. They’re tiny acts of returning, practiced daily.
The alternative is waiting for the crisis. Waiting until the debt is so deep that refactoring requires buy-in and resources and months of calendar time. But if you practice the small returns daily, the big crisis often never arrives. The codebase stays healthy because you never let it get sick.
This is the sustainable path. The dramatic stories, the automation engine, the database migration, those happened because we let things go too long. The billing system plugin was different. We caught it at year three instead of year seven. The refactor took weeks, not months. The practice of noticing, of making small improvements continuously, is what keeps the dramatic interventions rare.
The Shaping
Everything I’ve described so far assumes you wrote the code. But increasingly, you didn’t.
AI generates code faster than any human. It produces first drafts at volume, working solutions to the immediate problem. What it doesn’t produce is coherence.
AI has no sense of how one module relates to another. It doesn’t feel the friction building up across the codebase. It can’t tell when a pattern is emerging that deserves to be extracted, or when an abstraction has outlived its usefulness. It optimizes for the prompt, not for the system.
This makes refactoring essential to the human-AI partnership. The AI provides raw material. The human shapes it. Chapter 8 talked about beautiful code as guidance for your AI partner. Refactoring is how you maintain that guidance over time. It’s how you keep the codebase coherent as AI-generated code accumulates.
Without refactoring, AI output drifts toward local correctness and global chaos. Each piece works. The whole becomes unmaintainable. The human’s job is to see the whole, to feel where the structure wants to go, and to return the code to alignment.
Refactoring is how you teach your tools what you mean.
The Return
Refactoring is returning.
You return to code you wrote weeks or months ago. You return with new understanding, new context, new clarity about what the system actually needs. And in returning, you see what was always there, waiting to be recognized.
The hardest part is giving yourself permission. Deadlines loom. Features wait. Refactoring feels like indulgence when the backlog is full. But the backlog will always be full. The question is whether you’ll move through it with a codebase that helps you or one that fights you. Refactoring is maintenance of your ability to keep building.
The billing system’s plugin architecture wasn’t invented. It was discovered. The shape was latent in the problem from the beginning. We just couldn’t see it until we’d walked the terrain enough times. The refactor removed everything that obscured what the code wanted to be.
This is the deeper truth about refactoring. The goal is alignment: code that reflects understanding. The understanding was always the point. The code is just how we express it.
Chapter 8 talked about beautiful code, the kind that explains itself, that feels inevitable. That code doesn’t arrive beautiful. It becomes beautiful through refactoring. The clean routing system, the elegant abstraction, the module that reads like prose: these weren’t written that way on the first pass. They were discovered through the process of returning, seeing more clearly, and reshaping until the code matched what the author finally understood.
When you refactor, you honor your present clarity. I know more now, and the code should reflect that. The willingness to return and reshape is evidence of growth, not an admission of earlier failure. It’s the natural cycle of building anything worth building.
The first draft explores. The refactor returns. And in returning, you know the code for the first time.