Why Most Refactoring Efforts Stall
Start with a Map, Not a Module
The Correct Order: Stability Before Clarity
Phase 1: Establish a Safety Net
Phase 2: Isolate the Chokepoints
Phase 3: Decouple Before You Rewrite
Phase 4: Refactor High-Traffic Paths
Phase 5: Address Technical Debt in the Periphery
When to Stop Refactoring Legacy Code
Refactoring Versus Modernisation: Knowing the Difference
The Role of AI in Legacy Code Analysis
Common Sequencing Mistakes
Practical Starting Points for Different Codebases
Working With an External Engineering Partner
Frequently Asked Questions
Refactoring legacy code is one of the most consequential technical decisions a scaling company makes. Get the order wrong and you spend months touching code that still breaks in production. Get it right and you reduce risk, ship faster, and give your engineers back the confidence to move quickly.
This article covers how to approach refactoring legacy code methodically: where to begin, why sequence matters more than most teams expect, and how to know when to stop.
Why Most Refactoring Efforts Stall
Teams usually start in the wrong place. A developer finds a module they dislike, rewrites it, and the rest of the codebase continues to decay. Six months later, the product is still brittle and there is little to show for the effort.
The problem is not motivation. It is the absence of a prioritisation framework. Refactoring legacy code without a clear order is like renovating a building while the foundations are cracking — you can repaint the walls, but the structural risk remains.
The right starting point is not the code that annoys you most. It is the code that blocks everything else.
Start with a Map, Not a Module
Before touching a single file, you need to understand the shape of the system. That means mapping dependencies: which components are called most frequently, which carry the highest business risk, and which are so tightly coupled that changing one breaks three others.
A dependency map does not need to be exhaustive. It needs to be honest. The goal is to identify two things:
High-traffic, high-risk paths — code that runs on every request or handles money, authentication, or data integrity
Chokepoints — modules that everything else depends on, where a bug has the widest blast radius
With that picture in place, sequencing decisions can be made on the basis of risk and impact rather than aesthetics.
The Correct Order: Stability Before Clarity
Most engineers want to start with clarity — cleaner naming, better structure, more readable logic. That instinct is understandable, but it is backwards for legacy systems under active use.
The correct order is:
Phase 1: Establish a Safety Net
You cannot refactor safely without tests. If the codebase has no test coverage, the first task is not to change the code — it is to characterise it. Write tests that describe what the system currently does, even if what it does is wrong. These are called characterisation tests, and they exist to tell you when a refactor breaks existing behaviour.
This phase feels slow. It is not optional.
Phase 2: Isolate the Chokepoints
With a basic safety net in place, identify the modules that everything else depends on. These are not necessarily the largest or most complex files — they are the ones with the most inbound dependencies. Stabilise these first. That means reducing their surface area, removing hidden side effects, and making their interfaces explicit.
The goal is not to make them beautiful. It is to make them predictable.
Phase 3: Decouple Before You Rewrite
The most common mistake in legacy refactoring is rewriting a module before decoupling it from its neighbours. If a module is tightly coupled to five others, rewriting it in isolation means you are still inheriting all the coupling assumptions. The new code is cleaner, but the system is not.
Decouple first. Extract interfaces. Introduce seams — points in the code where one implementation can be substituted for another. Then rewrite.
This is the logic behind the strangler fig pattern: grow the new system around the old one, routing traffic gradually, until the legacy component can be removed without a big-bang migration.
Phase 4: Refactor High-Traffic Paths
Once the chokepoints are stable and the high-risk modules are decoupled, you can begin refactoring the paths that users actually hit. These are the areas where improvements in code quality translate directly into performance, reliability, and maintainability.
Work outward from the core. Refactor the modules serving the highest-volume user journeys before those serving edge cases.
Phase 5: Address Technical Debt in the Periphery
The outer layers of a legacy system — utility functions, helper modules, rarely-used features — are where most of the aesthetic debt lives. This is where you clean up naming, remove dead code, and improve readability. It is important work, but it belongs at the end of the sequence.
Doing it first is the engineering equivalent of tidying your desk while the server is on fire.
When to Stop Refactoring Legacy Code
This question matters more than most teams acknowledge. Refactoring without a clear stopping condition becomes an indefinite maintenance burden that competes with feature delivery.
Stop refactoring a component when three conditions are met: the code is covered by tests, its interfaces are explicit and stable, and engineers can change it without fear. That is the bar. Not perfection. Not elegance. Changeability.
If a module meets those three conditions, further refactoring is discretionary. For long-lived, high-traffic components it may still be worth doing. For peripheral code, the cost rarely justifies the benefit.
There is also a harder stopping condition: when the cost of incremental refactoring exceeds the cost of replacement. If a module is so deeply coupled, so poorly understood, and so central to the system that every refactor attempt creates new risk, the right move may be a more structured modernisation approach rather than continued surgical intervention.
Refactoring Versus Modernisation: Knowing the Difference
Refactoring improves existing code without changing its external behaviour. Modernisation changes the architecture, the platform, or both. They are different mandates and they require different plans.
If your codebase has reached the point where refactoring is producing diminishing returns, it is worth reading about the four software modernisation approaches and how to choose between them. The decision depends on your timeline, your risk tolerance, and how much of the existing system is worth preserving.
For teams that want a structured path through that decision, the 12-week legacy modernisation framework provides a phased approach to moving from an unstable legacy system to a maintainable, production-ready architecture.
The Role of AI in Legacy Code Analysis
Initial analysis is one area where the sequencing question has become significantly more tractable. AI agents can now process large codebases and surface dependency maps, dead code, and high-risk coupling patterns in hours rather than weeks. We have deployed Claude-integrated agents that produced a 57-page codebase analysis in 3 hours — work that would previously have occupied a senior engineer for the better part of a sprint.
This does not replace the engineering judgement required to act on that analysis. But it accelerates Phase 1 significantly, which means teams reach the actual refactoring work faster and with better information. You can see how this applies in practice in our AI agents for legacy code analysis article.
Common Sequencing Mistakes
Even experienced teams make predictable errors when approaching legacy refactoring. The most frequent ones:
Starting with the most visible debt. The code that looks worst is not always the code that causes the most problems. Prioritise by risk and dependency, not appearance.
Refactoring without tests. Every change to untested legacy code is a bet. Sometimes you win. Over a long refactoring effort, the probability of introducing regressions without a test suite approaches certainty.
Decoupling and rewriting simultaneously. These are two separate operations. Combining them doubles the surface area of the change and makes it much harder to isolate the source of any new failures.
Treating refactoring as a sprint. Legacy code accumulated over years. A two-week sprint will not resolve it. Refactoring needs to be embedded into the normal development rhythm — a proportion of each sprint allocated to systematic improvement, not treated as a one-off initiative.
Skipping the dependency map. Teams that skip the mapping phase tend to refactor the same modules repeatedly while the real chokepoints remain untouched. The map is not overhead. It is the plan.
Practical Starting Points for Different Codebases
The right first move depends on the state of the system:
No test coverage, high coupling: Start with characterisation tests on the highest-traffic paths. Do not refactor a line until you have a baseline.
Some tests, clear chokepoints: Move to Phase 2. Stabilise the modules with the most inbound dependencies before anything else.
Tests exist, architecture is the problem: You are ready for decoupling work. Introduce seams, extract interfaces, and begin routing traffic to new implementations incrementally.
Architecture is sound, code quality is the issue: This is the most straightforward scenario. Work outward from the core, refactoring high-traffic paths before peripheral utilities.
The sequence is not arbitrary. Each phase creates the conditions that make the next phase safe.
Working With an External Engineering Partner
For many scale-ups, the challenge is not knowing what to do — it is having the capacity and seniority to do it without halting feature delivery. Legacy refactoring requires engineers who can hold the full system in their heads, make architectural decisions under uncertainty, and maintain momentum across weeks rather than days.
If your team is stretched, or a lead engineer departure has left a knowledge gap in a critical part of the codebase, bringing in an embedded engineering pod with experience in legacy systems can compress the timeline significantly. The combination of senior technical strategy and hands-on delivery within a single engagement is what makes that work.
If you are at that point, wireapps.co.uk is a useful starting point to understand what that kind of engagement looks like in practice.
Frequently Asked Questions
What is the first step in refactoring legacy code?
The first step is mapping the system's dependencies to identify chokepoints and high-risk paths. Before writing any new code, you need to understand which modules carry the most business risk and which ones everything else depends on. That map determines your sequencing.
Do you need tests before refactoring legacy code?
Yes. Without tests, there is no way to verify that a refactor has not changed the system's behaviour. Characterisation tests — which describe what the system currently does rather than what it should do — are the minimum safety net required before making structural changes to untested code.
What is the difference between refactoring and rewriting?
Refactoring improves the internal structure of existing code without changing its external behaviour. Rewriting replaces a component with a new implementation. Rewriting is higher risk and higher effort; it is appropriate when a module is so deeply coupled or poorly structured that incremental improvement is no longer viable.
When should you stop refactoring legacy code?
Stop when the code is covered by tests, its interfaces are explicit and stable, and engineers can change it without fear of cascading failures. Perfection is not the target. Changeability is. Beyond that threshold, further refactoring is discretionary and should be weighed against the cost of feature delivery.
What is a chokepoint in a legacy codebase?
A chokepoint is a module with many inbound dependencies — code that a large number of other components rely on. When a chokepoint changes unexpectedly, the blast radius is wide. Stabilising chokepoints early in a refactoring effort reduces systemic risk across the whole codebase.
What is the strangler fig pattern and when does it apply?
The strangler fig pattern involves building a new system incrementally alongside the old one, routing traffic to the new implementation gradually until the legacy component can be removed. It applies when a module is too risky to rewrite in a single operation but too costly to leave in place. It is particularly useful for high-traffic, business-critical components.
How long does refactoring legacy code typically take?
There is no fixed timeline — it depends on the size of the codebase, the depth of coupling, and the existing test coverage. What is consistent is that refactoring done as a one-off initiative almost always underdelivers. Embedding it into the normal development rhythm, with a proportion of each sprint allocated to systematic improvement, produces more durable results than treating it as a discrete project.
Share




