Refactor or Rewrite?
A Decision Framework
for Unstable Software
VNFlatform Engineering Team
When software is unstable, a rewrite can feel like the cleanest answer. The current code is difficult to explain, defects recur, and every change creates another surprise. But replacing everything also discards working behavior, creates a second system to verify, and moves risk into data migration and rollout.
The useful decision is rarely “keep everything” or “rewrite everything.” Decide by workflow and system boundary: what should be preserved, what needs stabilization, what can be refactored in place, what should be replaced, and whether any bounded area truly requires a rewrite.
Begin with the outcome, not the code preference
State what the product must do that it cannot do reliably today. The driver may be recurring incidents, blocked feature delivery, security exposure, unsupported technology, performance limits, a costly vendor dependency, or an inability to release safely. Each driver leads to a different intervention.
A rewrite justified only by “the code is messy” has no acceptance boundary. A decision tied to an observable constraint can be tested.
Audit the system that actually exists
Before selecting an approach, reproduce the build, map deployed components, trace critical workflows, inspect data and integrations, and identify operational ownership. The Inherited Codebase Audit Checklist provides that evidence path.
Unknown behavior is not proof that the code should be replaced. It is a reason to investigate before estimating either option.
Option 1: Preserve stable behavior
Keep a component when its behavior is understood, important workflows are reliable, the technology remains supportable, and change demand is low. Add documentation and monitoring if ownership is the real gap.
Preservation is an active decision, not neglect. Record the boundary, its dependencies, known limits, and conditions that would trigger a later change.
Option 2: Stabilize before restructuring
Stabilization reduces immediate risk without redesigning the system. Typical work includes reproducing failures, protecting data, repairing authorization, adding checks around critical flows, making deployments repeatable, improving error visibility, and documenting recovery.
This is often the shortest route when users already depend on the product. It creates the safety net needed for later refactoring or replacement.
Option 3: Refactor in place
Refactoring changes internal structure while preserving externally observable behavior. It fits when the current behavior is valuable, tests or other verification can protect it, and the architecture can support the next set of requirements after focused changes.
- Separate duplicated or conflicting business rules.
- Move trust decisions to a controlled server boundary.
- Isolate external integrations behind explicit adapters.
- Break a high-change module into testable responsibilities.
- Remove dead paths and unused dependencies after evidence confirms they are unused.
Refactor around the work the product needs. A repository-wide cleanup without a delivery objective can consume time without changing risk.
Option 4: Replace a bounded component
Selective replacement fits when one component has disproportionate risk, cannot be supported, or has a clear contract with the rest of the system. Examples may include an abandoned integration, an unsafe authentication layer, or a job runner that cannot meet operational requirements.
Define input, output, data ownership, failure behavior, migration, and rollback before building the replacement. A smaller boundary is easier to compare against existing behavior.
Option 5: Rewrite a justified boundary
A rewrite may be appropriate where the existing boundary cannot satisfy required security, reliability, platform, or operating constraints without comparable or greater change. It may also be justified when the old component cannot be built, tested, or legally operated and no maintainable recovery path exists.
The rewrite still needs acceptance criteria, data migration, integration compatibility, operational readiness, rollout, and a fallback. It is a new delivery project, not a deletion of old risk.
Compare the risks on both sides
- Behavior: Which undocumented rules must remain?
- Data: How will records, history, identifiers, and in-flight work move?
- Delivery: Can product work continue during the change?
- Operations: Can both old and new paths be monitored and supported?
- Rollback: What happens when the new path fails after real use begins?
- Ownership: Does the team understand the new system better than the old one?
A rewrite wins only when its complete transition risk is lower than a credible stabilization or refactoring path—not when its new code looks cleaner in isolation.
Prefer incremental modernization where the boundary allows it
Incremental replacement can route selected behavior to a new component while the existing system continues to serve the remaining paths. AWS documents this approach as the strangler fig pattern, emphasizing gradual migration rather than a single big-bang cutover.
The pattern is not automatically correct, and it does not require microservices. Its useful principle is to move one verifiable boundary at a time and retire the old path only after the new one is proven.
Use a decision record for each boundary
For every area under review, record the current constraint, evidence, options considered, chosen action, rejected alternatives, prerequisites, success criteria, rollback, owner, and review date. This keeps “temporary” modernization choices from becoming unexplained architecture.
Need evidence before choosing refactor or rewrite?
VNFlatform's AI Code Rescue & Product Hardening path starts with a paid technical audit. The audit identifies what can be preserved, what needs containment or stabilization, and which bounded changes should be estimated.
If scaling pressure is driving the decision, also review Technical Debt in an MVP: What Must Be Fixed Before You Scale.
The practical decision rule
Preserve what is understood and reliable. Stabilize what users depend on. Refactor where behavior should stay but structure blocks change. Replace or rewrite only the smallest boundary whose existing constraints cannot be resolved responsibly.