Deal with bad code by making change safer and more incremental: first protect the behavior that matters, then identify a useful seam, refactor in small steps, keep cleanup distinct from feature behavior, and improve the code as you work in it. A rewrite can be justified, but it is not the default cure for a difficult codebase; the choice depends on evidence about risk, dependencies, and the cost of maintaining either path.
1. Build a safety net before changing structure
Before reorganizing code, record what it currently does in the paths that must continue working. Characterization tests capture existing behavior—including behavior that may be awkward or undocumented—so you can detect unintended changes during refactoring. Add automated tests for important user and system paths; where relevant, include performance checks or security threat analysis.
Martin Fowler’s overview of refactoring explains why production code should have automated tests that detect errors during change and clarify how internal structures are used: Definition of Refactoring. His code-review guidance also describes the roles of testing, performance tests, and threat analysis in giving confidence and identifying areas that need extra attention: Code Review.
If the system has little or no test coverage, do not treat that as a reason to make a sweeping change quickly. Start with the most consequential behavior you can observe and verify. In some cases, a small manual regression checklist or a test around one critical workflow is a practical first step toward a stronger automated safety net.
#1 Best Overall
2. Map the problem and choose seams deliberately
Understand how the code behaves and what depends on it before deciding what to replace. Combine static analysis with runtime evidence such as logs, dependency information, and repository history. The goal is not to catalogue every imperfection; it is to find a seam where a change can be isolated, checked, and rolled back without needlessly disturbing unrelated parts.
- Look for complexity and coupling. Static analysis can identify complicated or highly connected classes that are likely to make changes risky. Microsoft’s guidance on code cleanup recommends using analysis to locate these areas: Code cleanup in Visual Studio.
- Check observed behavior. Logs and runtime analysis can reveal paths that are important in production but not obvious from reading the code alone.
- Trace dependencies and history. Find callers, data contracts, and prior changes around the area. This helps distinguish an isolated module from a change that could affect many consumers.
- Keep changes incremental and version-controlled. IEEE guidance on large codebases identifies static analysis, automated refactoring engines, and incremental version-controlled changes as useful practices: Refactoring Large-Scale Codebases.
A high-complexity file is not automatically the right place to begin. Prefer a boundary where behavior is understood, dependencies are manageable, and tests can tell you whether the change worked.
Rank #2
3. Refactor in small, behavior-preserving steps
Refactoring improves the internal design of existing code without changing its externally observable behavior. Fowler describes it as “a controlled technique for improving the design of an existing code base” in Definition of Refactoring. The practical consequence is to make a sequence of small transformations rather than combine many structural changes into one hard-to-diagnose edit.
- Choose one small change with a clear purpose, such as extracting a method or clarifying a dependency.
- Run the relevant tests and checks before and after the change.
- Review the diff to confirm that the change is structural rather than an unnoticed behavior change.
- Commit or otherwise preserve the verified step before moving on.
Keep each change narrow enough that a reviewer can understand what moved and why. If a test fails, a small step makes it easier to locate the cause; if the change creates a problem, it is easier to revert without discarding unrelated work.
Rank #3
4. Separate structural cleanup from feature behavior
When a feature requires touching messy code, avoid hiding a broad refactor inside the feature change if you can separate the work. A preparatory cleanup can make the later behavior change easier to reason about; distinct changes also make review more precise.
Gerrit’s review guidance recommends aligning a change’s scope with its purpose and notes that a separate refactor is easier to review, helping reviewers spot errors: Submitting changes. Microsoft Research likewise argues that code review should be systematized and applied precisely, since review takes time and has costs: Code Review: Systematically and with More Precision.
Rank #4
There are exceptions: a tiny cleanup may be inseparable from the feature, or splitting the changes may create more coordination overhead than clarity. Use the review diff as a test: can the reviewer distinguish the intended behavior change from the code movement or renaming? If not, split the work where practical.
5. Pay down debt continuously where work already touches it
Do not wait for a dedicated cleanup project to improve every rough edge. When a bug fix or feature brings you to a small, clear improvement, leave that area easier to understand than you found it. Fowler’s “boy scout rule” makes the same point: taking opportunities to clarify code as you encounter it helps prevent gradual deterioration and makes later refactoring less difficult: Opportunistic Refactoring.
Best Value
Keep the improvement proportionate. A focused rename, clearer condition, or removal of an obvious duplication may fit alongside a task; a redesign of a shared subsystem probably deserves its own scope and review. Microsoft’s code-cleanup guidance also recommends checking an improvement backlog when new or modified work enters an area: Code cleanup in Visual Studio.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Refactor, rewrite, or contain?
There is no universal winner. The cited guidance supports incremental refactoring and analysis, but it does not establish that refactoring always beats replacement. Compare the options against the actual system and its risks:
| Option | Behavior safety | Test coverage needed | Time to first value | Rollback difficulty | Dependency risk | Ongoing maintenance |
|---|---|---|---|---|---|---|
| Targeted refactor | Can be managed through small, behavior-preserving changes and checks. | Tests around affected behavior make each step safer. | Potentially early: improvements can be delivered one seam at a time. | Usually limited when each step is narrow and version-controlled. | Depends on the seam; mapping callers and coupling helps bound it. | May reduce maintenance burden without replacing working behavior. |
| Larger rewrite | Harder to establish until the replacement has been compared against existing behavior. | Requires a way to verify important behavior across old and new implementations. | May be delayed while substantial functionality is rebuilt. | Can be difficult after the old system or its interfaces have been removed. | Potentially broad when integrations, data, or consumers must change together. | Depends on whether the new system actually resolves the causes of the old burden. |
| Containment | Limits direct change to risky code, but leaves its behavior and constraints in place. | Tests remain important at the boundary where other components interact with it. | Can provide a local workaround without broad internal change. | Often easier for an isolated workaround, depending on the boundary. | May be reduced by isolating dependencies, though they are not eliminated. | Can preserve a maintenance burden if the contained area continues to grow or needs frequent changes. |
These are decision prompts, not measured guarantees. Before choosing a rewrite, identify specific evidence that incremental change cannot address the problem—for example, a boundary that cannot be maintained or a dependency constraint that makes local changes impractical. If that evidence is absent, a targeted refactor or containment step can reduce risk while you learn more about the system.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

