Setting the scene
We had a 12-year-old monolith that everyone loved to hate and hated to touch.
You know the type — the kind where utils.ts is 4,000 lines and contains,
in no particular order: a date formatter, the entire billing logic, a
half-finished feature flag system, and a function literally named
doTheThing() with no comments.
Every quarter, someone would propose The Big Rewrite. Every quarter, we politely declined, because The Big Rewrite is how engineering orgs go bankrupt with style.
So instead, we did the boring thing. We refactored it incrementally over
eighteen months, and by the end, the monolith had — metaphorically — filed a
restraining order against us. It just wanted to be left alone in its little
legacy/ folder. Honestly, fair.
The plan (such as it was)
We had three rules, written on an actual whiteboard, that we did not allow ourselves to break:
- Carve out clear domain boundaries first. Before moving a single line, we drew the map. Billing, identity, notifications, reporting. If we couldn't agree on what belonged where, we weren't ready to move it.
- Extract the lowest-risk module first. Not the most exciting one. Not the one with the worst code. The one most likely to succeed. Wins build momentum; failures build resignation.
- Resist the urge to rewrite "while we're in there." This was the hardest rule. We extracted as-is first, then improved in a follow-up PR. Mixing extraction with redesign is how you end up six months in with nothing shippable.
What actually went wrong
Plenty.
- We underestimated how many things imported
utils.tstransitively. ("Justutils, how bad could it be?" Reader: it was bad.) - Our first extraction broke a cron job nobody remembered owning. We learned about it from a customer. That was a fun Tuesday.
- We tried to land a 2,000-line PR. It sat in review for nine days. We never did that again.
Each of these became a process change: better dependency mapping, a runbook for "who owns this thing?" archaeology, and a hard PR-size budget.
What actually went right
The boring stuff:
- Shipping weekly. Every Friday, something small landed in production. Not a milestone. Just a thing.
- Feature flags on every extraction. If the new module misbehaved, we flipped back to the old code path in seconds, not hours.
- Pairing across teams. The engineers who knew the old code paired with the engineers who'd own the new module. Knowledge transfer happened as a side effect of the work, not as a separate meeting.
The lesson
Refactors fail when they try to be heroic. They succeed when they're boring, incremental, and shippable on a Tuesday afternoon. The monolith didn't need to die. It just needed a few things to move out so it could finally relax.
If you're staring down a similar codebase: make the map, pick the easy win, and don't try to rewrite the world while you're holding a screwdriver. Future-you — and the on-call engineer at 3am — will thank you.