Every long-lived software system eventually reaches a moment when someone says, “We should rewrite this.” The code is difficult to change. The original team is gone. Tests are thin. The framework is old. Features that sound simple take weeks.
The frustration may be completely justified. The proposed solution still deserves skepticism.
Old software contains two things at once: accumulated technical debt and accumulated business knowledge. A rewrite can erase both.
The clean-slate advantage is temporary
A new codebase feels fast because it begins without production history. There are no strange customer records, old integrations, partial migrations, or exceptions created by years of business decisions. The team can draw clear boundaries and use current tools.
Then reality arrives. The new system has to reproduce the behavior people depended on, including behavior nobody documented. It has to migrate data, run alongside the old system, satisfy new requests, and eventually switch traffic without losing trust.
This is where many rewrites slow down. The difficult part was not typing better code. It was discovering what the existing software actually did.
Diagnose the pain precisely
“The system is a mess” is not specific enough to justify a multi-month investment. I want to know where the cost is coming from.
- Are releases risky because there are no automated tests?
- Is one module slowing every feature because its boundaries are unclear?
- Is the database model fighting the current business?
- Is the platform unsupported or creating a security problem?
- Are performance limits architectural, or is one query responsible?
- Does the team lack documentation and operational visibility?
Each answer suggests a different intervention. Tests, observability, a module extraction, a data migration, or a targeted replacement may solve the expensive problem without replacing everything.
Recover the business rules first
Before changing a critical system, I map its behavior. That includes workflows, external dependencies, data ownership, failure paths, permissions, scheduled jobs, reports, and manual steps that happen outside the code.
Tests can act as a form of excavation. Characterization tests record what the system does today, even when that behavior is not ideal. Logs and production traces reveal paths the team forgot existed. Conversations with support and operations often surface rules that are absent from technical documentation.
This work is useful whether the final decision is a rewrite or an incremental modernization. It turns assumptions into an inventory.
Create seams before replacing parts
A system is easier to modernize when change can happen behind a stable boundary. An API, event stream, adapter, or clear module interface can become that seam.
Once the seam exists, the team can replace one capability at a time. New behavior moves to the new component. Existing behavior continues to run. Traffic can shift gradually. Results can be compared. Rollback remains possible.
This approach is less dramatic than announcing a new platform. It is often much safer. It also starts delivering value before the entire program is complete.
Treat data migration as a product
Rewrites often underestimate data. Old records contain missing fields, retired formats, duplicate identities, and states the new model did not anticipate. A one-time migration script is rarely enough for a system that must stay available while both versions evolve.
I design migration with the same care as application code: repeatable transformations, validation, reconciliation reports, versioned mappings, and a rollback plan. The business needs to know what moved, what did not, and why.
If the migration plan is vague, the rewrite plan is not ready.
When a rewrite is justified
Sometimes replacement is the responsible choice. The existing platform may be unsupported, the data model may be incompatible with the new business, security boundaries may be impossible to repair, or the cost of every incremental change may exceed the cost of running two systems during a transition.
Even then, I prefer a staged replacement with measurable exit criteria. Define what success means, identify the first useful slice, prove migration on real data, and make the cutover reversible until confidence is earned.
Choose progress over purity
The question is not whether the current code deserves to survive. Code does not have feelings. The question is which path improves the product while managing business risk.
A targeted modernization can look untidy during the transition. That is acceptable. Architecture is not a contest for the cleanest diagram. It is the practice of moving a working business from its current constraints toward a better set of options.
Rewrite when the evidence supports it. Otherwise, find the expensive boundary, make it visible, and improve the system one accountable step at a time.