The most expensive decision in software
Every company with software older than three years faces this moment. The codebase is brittle. New features take three times as long as they should. The lead developer is the only person who understands it — and they are updating their LinkedIn.
The question: do you refactor incrementally, or do you rebuild?
Get this wrong and you lose a year of progress. We have watched companies make both mistakes. Here is how to make the call correctly.
When refactoring is the right answer
Refactoring makes sense when:
The architecture is fundamentally sound but the execution is messy. Business logic is correct, data model is right, but the code is hard to read and poorly tested. This is fixable without rebuilding.
There are clear seams. You can isolate the worst parts without touching everything else. If you can unit test a module before rewriting it, you can refactor it.
The team knows the codebase intimately. Refactoring requires understanding what exists. If institutional knowledge is intact, incremental improvement is viable.
The technology stack is still viable. If you are on React 16 with legacy hooks patterns, refactoring to modern patterns is straightforward. If you are on a framework with no community support, refactoring buys you time but not safety.
When rebuilding is the right answer
Rebuilding is the right answer more often than developers want to admit, and less often than business stakeholders want.
Rebuild when:
The data model is wrong. This is the most important criterion. Bad architecture at the data layer means every feature fights the database. You cannot refactor your way out of a wrong schema — you rewrite it.
The codebase has no test coverage and no one understands it. Refactoring untested code is the same as rewriting it, but slower and with more risk. If you cannot verify that your changes are safe, you are not refactoring — you are guessing.
The technology is end-of-life. Legacy PHP from 2010, Rails 4 with no upgrade path, or an abandoned framework. Technical debt compounds faster than business value when the foundation is unsupported.
The original scope was completely different. A system built for 50 users being stretched to serve 500,000 is not fixable by refactoring — the load assumptions baked into the architecture are wrong at a fundamental level.
The team that built it is gone. No documentation, no tests, no tribal knowledge. In this scenario, refactoring is archaeology. You will spend as much time understanding what exists as you would building from scratch.
The strangler fig pattern: the third option
In many cases, the right answer is neither a full rebuild nor pure refactoring. It is the strangler fig pattern: wrap the legacy system while building the replacement alongside it.
The approach:
- Identify the highest-value, highest-pain module
- Build the new version in isolation, behind a feature flag or separate route
- Move traffic to the new version incrementally
- Deprecate the old version module by module
This reduces risk dramatically. You are never in a state where nothing works. You always have a rollback path.
The catch: it requires discipline. The strangler fig fails when teams add features to both the old and new system simultaneously. Pick a direction and hold it.
The questions to ask before deciding
Before making the call, answer these:
- What is the current cost of the legacy system in developer hours per week?
- Is the pain localized (one module) or systemic (everything)?
- What is the test coverage? Is it meaningful or decorative?
- Is the data model still correct for the current use case?
- How long until the technical debt causes a customer-facing incident?
- What is the team's confidence in their ability to understand the current code?
The answers will point you to the right decision 80% of the time. The remaining 20% requires a technical audit.
What a technical audit actually involves
If you are unsure, bring in a technical audit before committing to either path. A proper audit covers:
- Database schema review and query performance analysis
- Dependency audit (outdated packages, security vulnerabilities)
- Architecture review (is the system layered correctly?)
- Test coverage analysis
- Code complexity metrics (cyclomatic complexity, coupling, cohesion)
- Load and performance baseline
The audit should produce a recommendation with a cost estimate for both paths and a time-to-value comparison. If someone gives you a recommendation without the cost comparison, they are guessing.
We run these audits before taking on any legacy modernization project. The cost of the audit is typically 2-5% of the project cost. The alternative — guessing — costs far more.