Stop Rewriting, Start Steering
"Make the change easy, then make the easy change." — Kent Beck
In a previous article, I argued that legacy code is a people problem, not a technology problem. A rewrite is rarely the answer. But if you're not rewriting, what are you doing? You still have a system that's hard to work with. You still have users who need things to get better. So how do you actually change a legacy system?
Two Extremes, Both Bad
There are two natural instincts when you're staring at a legacy system you need to improve.
The first is the big redesign. You study the system, identify the fundamental problems — wrong data model, bad architecture, tangled dependencies — and envision a comprehensive solution that addresses all of them at once. New schema. New service boundaries. New everything.
The problem with the big redesign is that it's a big redesign. It takes a long time to implement. During that time, the old system still needs to be maintained. You're running two tracks in parallel, and eventually one of them loses. Usually it's the redesign. It takes longer than expected, misses subtleties the old system handled quietly, and introduces new problems of its own. After months of work, you might end up roughly where you started — just with different problems.
The second instinct is pure incrementalism. Just fix what's in front of you. A bug here, a feature request there. Small changes, shipped fast. Users see improvements quickly.
The problem with pure incrementalism is that it can't fix fundamental issues. If the data model is wrong, no amount of small patches will make it right. You circle around a local optimum, making things slightly better in ways that never add up to the structural change the system actually needs. You're rearranging furniture in a building with a cracked foundation.
Optimization Landscape
If you've done any work in optimization, this should sound familiar. You have a solution space — all the possible states your system could be in. You have an objective — something like "how well does this system serve its users and its developers." And you have two classic search strategies that map directly to the two instincts above.
Pure incrementalism is gradient descent. You look at your current position, find the direction of steepest improvement, and take a small step. It's reliable and low-risk. But it converges to the nearest local optimum, which might be mediocre. If there's a much better solution that requires passing through a valley first — a temporary step backward — gradient descent will never find it.
The big redesign is a long jump to a distant point in the solution space. You've spotted what looks like a higher peak on the horizon and you're leaping for it. The potential payoff is big, but so is the risk. You might land on a peak, or you might land in a deeper valley than where you started. And the longer the jump, the less accurately you can predict where you'll land.
Neither strategy alone is sufficient. Gradient descent gets stuck. Long jumps are unpredictable.
Strategic Direction, Tactical Steps
What works is combining them. You need a strategic vision — a clear understanding of the fundamental problems and what a better system looks like. And you need tactical execution — small, incremental changes that ship value to users now.
The hard part is making the tactical steps move you toward the strategic goal. Every small change should do two things: solve an immediate problem for a user, and nudge the system closer to the target architecture. Not every change can do both, but that should be the default.
Here's what that looks like concretely. Say your e-commerce system has order validation logic duplicated across three services — the API, the batch processor, and the webhook handler. A customer reports that discount codes work in the UI but not when orders come through the webhook. The quick fix is to patch the webhook handler's copy of the validation logic. It solves the user's problem today. But it deepens the structural problem — now you have three copies of order validation that will continue to drift apart.
The strategic fix is to extract order validation into a shared module, have the webhook handler use it, and fix the discount bug there. It takes longer. But the next time a validation bug shows up, there's one place to fix it. And the next service that needs order validation doesn't have to copy-paste a fourth version. You've solved the user's problem and moved the architecture in the right direction.
This requires holding two things in your head at once: what the user needs today, and where the system needs to be in a year. It requires discipline to say no to quick hacks that solve the immediate problem but make the structural problem worse.
Know Where You're Going
You can't align your tactics with your strategy if you don't have a strategy. Before you start making changes, you need to answer some questions:
- What are the fundamental problems with the current system? Not symptoms — root causes.
- What would a better system look like? Not in detail, but directionally.
- Which of today's problems are caused by those root issues, and which are incidental?
The answers don't need to be perfect. They'll evolve as you learn more. But you need a working hypothesis. Without one, your incremental changes are random walks. With one, they're directed search.
Trade-offs
The strategic-plus-tactical approach is slower than pure incrementalism in the short term. Each change carries a bit of overhead because you're not just solving the immediate problem — you're solving it in a way that fits the bigger picture. That means more thinking per change. More design. Sometimes a more complex implementation than the minimum viable fix.
It's also less dramatic than the big redesign. You won't have a grand unveiling where everything is new and shiny. Progress is gradual. Stakeholders who want to see a transformed system next quarter will be disappointed.
What you get in return is steady, compounding improvement. Each change makes the next one easier. The system gets incrementally better in ways that reinforce each other. And you never stop delivering value to users while you do it.
The Takeaway
Changing a legacy system isn't about choosing between revolution and evolution. It's about having a clear destination and taking small steps toward it. Know where you're going. Make each change count twice — once for the user, once for the architecture.
It's slower than you'd like. It's less satisfying than a clean rewrite. But it works, and it keeps working, because it respects the reality that systems change one commit at a time.