The codebase was older than some of the people working on it.
Ten years of decisions, half of them made by engineers who'd long since left. No tests in the scary parts. Comments that lied. A function so central and so terrifying that the team had an unwritten rule: nobody touches processOrder after Wednesday.
My job was to modernize it without breaking the business that ran on it. My first instinct — the instinct everyone has — was to throw it all away and rewrite from scratch. That instinct is almost always wrong, and learning why was the most valuable part of the whole project.
To refactor an old codebase safely, don't rewrite it — strangle it. Wrap the legacy system, route new and changed functionality through fresh well-tested code, and replace the old parts piece by piece while everything keeps running. Add characterization tests before you change anything, work in tiny reversible steps, and respect that the messy code encodes years of hard-won edge cases.
The full rewrite is the most seductive and most dangerous option.
It sounds clean: stop, build a beautiful new version, switch over, done. In reality, you spend a year or more rebuilding what already works while the old system still needs bug fixes and new features. You're maintaining two systems and shipping nothing. Worse, you slowly rediscover that every ugly bit of the old code was ugly for a reason — a real edge case, a real customer, a real 2 a.m. incident — and you have to re-learn each one the hard way.
The legacy code looks like a mess. A lot of that mess is actually a decade of bug fixes, encoded. Choosing the boring, reversible path over the heroic rewrite is exactly the kind of judgment I keep coming back to in the brutal truth about becoming a senior developer — the senior move is usually the unglamorous one. Martin Fowler's writing on the strangler fig pattern is where I first saw this incremental approach named and argued properly.
Old code is ugly because it survived contact with reality. A rewrite throws away the scar tissue and re-earns every wound.
Photo by Alexandre Debiève on Unsplash
Instead of replacing the system, you grow a new one around it until the old one withers. The name comes from a vine that grows around a tree and eventually stands on its own.
In practice it looked like this:
At every single point, the whole thing kept running. We were never in a "it's all broken until the rewrite lands" state, because there was no big-bang switch. Just a steady, boring migration with a working product the entire way.
You cannot safely change code you don't understand, and the old code was full of behavior nobody could explain. The fix wasn't to understand it — it was to pin it down.
Before touching a scary module, we wrote characterization tests: tests that capture what the code currently does, bugs and all. We weren't asserting what it should do. We were photographing its actual behavior so we'd know the instant a refactor changed it.
This felt strange — writing tests that locked in known-weird behavior. But it was the safety net that made everything else possible. It's the same lesson I learned the slow way and wrote up in the testing habit I wish I'd started earlier: the tests aren't bureaucracy, they're what lets you move fast without fear. With those tests in place, we could rip the internals apart and instantly see if any externally-visible behavior shifted. Refactoring without that net is just hoping.
| Tempting move | Safer move |
|---|---|
| Rewrite the whole thing | Strangle it piece by piece |
| Change first, test later | Characterization tests first |
| One giant migration | Many tiny reversible steps |
| "Clean up" mystery code | Pin its behavior, then change |
Photo by John Schnobrich on Unsplash
The biggest mistake available was trying to do too much per change.
Every step we took was small enough to review fully, ship independently, and roll back in minutes. Move one function behind the wall. Ship. Confirm. Next. It felt slow on any given day and was astonishingly fast across the quarter, because we never lost a week untangling a giant tangled merge or chasing a bug across a thousand-line change.
Small steps also kept the team sane. There's a morale cost to a refactor that never visibly progresses. Shipping a real improvement every couple of days — even a tiny one — kept everyone believing the thing was actually getting better, because it visibly was.
We leaned on a bit of automation to make the small steps cheap: scripts that compared old and new outputs on real traffic, so we got an automatic warning the moment a migrated piece disagreed with its legacy original.
The last lesson was attitude. It's easy to sneer at old code and the people who wrote it.
But that code paid the bills for a decade. Its authors were solving real problems under real constraints with the tools they had, often under deadline pressure you weren't there for. The weird workaround you're about to "fix" might be load-bearing. Approaching the system with contempt is how you confidently delete the one line holding production together.
Curiosity beats contempt. Every time I found something baffling and asked "why would someone write this?" instead of "who wrote this garbage," I usually found a real reason — and avoided reintroducing the exact bug that line was fixing.
The hardest parts of this project weren't technical. They were about people, and I was completely unprepared for that.
A long refactor is a marathon with no obvious finish line, and the team's morale tracks the visible progress closely. Early on, when we were mostly building the wall and writing characterization tests, it looked from the outside like nothing was happening. Stakeholders got nervous. A couple of engineers privately wondered if we were just polishing while the backlog grew. That doubt is corrosive, and it almost killed the effort before it got going.
What saved us was making progress visible. We kept a simple board showing which pieces of the legacy system had been migrated and which were left — a literal strangler diagram that filled in over time. Watching the old box shrink, week by week, did more for morale than any status meeting. People could see the vine growing.
We also made a rule: every migrated piece had to deliver some small, visible win, not just "same behavior, cleaner code." A faster page here, a fixed long-standing bug there. That gave the business a reason to keep funding the work, because they were getting tangible improvements the whole way, not a promise of payoff at some distant end.
A refactor lives or dies on whether people can see it working. Invisible progress is, to everyone watching, indistinguishable from no progress.
The technical strategy was the easy half. Keeping people believing in a long, unglamorous migration — that was the part that actually decided whether it shipped.
If I could send one note back to the version of me who first opened that decade-old codebase, it would be short: you are not as smart as the mess in front of you.
I walked in assuming the previous engineers had been sloppy and that I'd swoop in and do it right. That arrogance would have wrecked the project. The code was tangled because the problem was tangled — a decade of real customers, real edge cases, and real deadlines, layered on top of each other. My job wasn't to prove I was better than the people who came before. It was to carefully, humbly, move their solution forward without losing what it had learned.
The second thing I'd tell myself: pick the strangler approach on day one and never waver. I spent my first weeks secretly fantasizing about a rewrite, and that fantasy slowed me down because part of me wasn't committed to the incremental path. Once I fully accepted that we were strangling, not rewriting, everything got easier. The plan was clear, the risk was bounded, and the product never stopped working.
The legacy system isn't your enemy. It's a survivor that's been keeping the lights on, and your job is to help it retire gracefully — not to shoot it.
A year and change later, the old box on our diagram was empty. We'd modernized a system that ran the business without a single big-bang outage. The rewrite I almost reached for would, statistically, never have shipped at all.
If you're staring down your own decade-old system, try strangling one small piece this sprint and see how much calmer the work feels — and stick around for more hard-won notes like this.
Q: When is a full rewrite ever the right call? Rarely — usually only when the underlying platform is truly dead or the system is small enough to rebuild in weeks, not years. For a large, business-critical system that still works, incremental strangling is almost always safer.
Q: How do I refactor code that has no tests? Add characterization tests first — tests that capture the code's current behavior, including its quirks. They let you change internals while instantly catching any shift in what the code actually does.
Q: How small should each refactoring step be? Small enough to review completely, ship on its own, and roll back in minutes. If a change is too big to fully understand in one sitting, it's too big — split it.
Q: What if I don't understand why the legacy code does something weird? Treat that as a warning, not an invitation to delete. Pin the behavior with a test, ask around, check the history. Weird old code is often weird because of a real edge case you haven't discovered yet.
Modernizing a decade-old system isn't a demolition — it's a transplant performed on a patient who has to keep working the whole time. Strangle, don't rewrite. Pin behavior before you change it. Move in tiny steps. And respect that the mess in front of you is a survivor.
The rewrite that never ships beats nothing. The slow migration that never stops beats everything.
What's the processOrder in your codebase that everyone's quietly afraid to touch?
No following, no network, no luck. Just an unglamorous system I ran for eighteen months. Here's exactly what I did.

I went from 200 to 11,000 subscribers without hiring anyone. AI didn't write my newsletter — it did everything around it.

I chased big, audacious goals for years and burned out every time. Then I built my whole life around wins so small they felt like cheating.

Comments
Sign in to join the conversation
No comments yet. Be the first to share your thoughts!