There’s a particular gravity that pulls engineers toward rewrites. The appeal is understandable—it feels clean and promises relief. Most of the time, however, a full rewrite represents unnecessary overhead. What typically helps more is a series of well-aimed refactors tied to customer-visible outcomes.
Before diving into any major changes, ask yourself what will materially improve after this change. You should be able to name an objective, customer-visible outcome. State your tradeoffs and non-goals explicitly because clarity prevents scope creep. Identify the smallest safe step you can take, preferring testable boundaries. Define measurable targets like latency, error rate, or lead time so you’ll know if the change worked. If you can’t answer these questions quickly, you probably don’t need a rewrite.
The refactoring approach I’ve used repeatedly starts with exposing boundaries. Extract functions, modules, or adapter layers, then add tests around these seams. From there, you can strangle the legacy code by routing a fraction of traffic or subset of cases to the new path. Monitor the results, then expand gradually.
Maintaining API stability throughout this process buys you safety and time. Backwards-compatible interfaces let you move confidently without breaking existing functionality. Time-box your exploration by working in branches for defined periods, but always document what you learned before committing to larger changes. Finally, celebrate deletions as much as additions because removing complexity is often more valuable than adding features.
This approach requires leadership support to work effectively. Protect focus by reserving capacity each sprint for quality and refactoring, treating it as product work rather than charity. Make the work visible through ADRs, decision logs, and clear PR descriptions so the story is obvious in the changes. Reward simplification by valuing fewer concepts and fewer configuration options. Pair on complex areas because two perspectives on high-risk changes pays dividends.
Sometimes a rewrite is the right move, but only under specific circumstances. Architectural mismatches involve fundamental constraints like moving from single-tenant to multi-tenant that can’t be safely iterated toward. Irreparable technical debt means no tests, no owners, no roadmap, and a proven inability to ship changes. Platform shifts unlock order-of-magnitude improvements that customers actually need. Even then, treat rewrites as series of strangler steps with milestones, shipping value along the way.
The fundamental insight is that refactoring compounds like interest while rewrites are lottery tickets. Most teams need more of the former. Pick one boundary, add a test, and make the code a little clearer than you found it.