Your legacy system works. That’s the problem.
It generates revenue, customers depend on it, and nobody wants to touch the payment module that Dave wrote in 2017 before he left. Every feature request takes three times longer than it should. New hires stare at the codebase for weeks before making their first meaningful commit. And somewhere in a Slack channel, an engineer is typing the word “rewrite” for the hundredth time.
I’ve been in this exact situation at multiple companies. At AutoScout24, we inherited systems from acquisitions that were built with completely different philosophies, tech stacks, and quality standards. The temptation to burn it all down and start fresh is always there. But after years of leading platform and engineering teams through these transformations, I can tell you: evolution almost always beats revolution.
This article is an updated version of the one I published in 2019. What’s changed since then is how we evolve these systems. AI-assisted development, platform engineering, and modern observability have fundamentally shifted what’s possible. A refactoring effort that would have taken a team months in 2019 can now be measured in weeks — sometimes days.
Making the Invisible Visible
Before you can fix a legacy system, you need to understand how broken it actually is. “It feels slow” and “the code is messy” aren’t arguments that will get you budget. You need data.
The metrics that matter haven’t changed much, but the tools have gotten dramatically better:
Code health: Static analysis tools like SonarQube or CodeClimate can quantify your technical debt in hours. AI-powered code analysis — through tools like GitHub Copilot, Sourcegraph Cody, or even a well-prompted Claude session — can now identify architectural smells that static analysis misses entirely. Feed your codebase to an LLM and ask it to map coupling between modules. The results might surprise you.
Deployment pipeline: How long from merge to production? If the answer is “days” or “it depends on who’s deploying,” you have infrastructure debt. DORA metrics (deployment frequency, lead time, change failure rate, mean time to recovery) remain the gold standard here.
Test coverage and runtime: Coverage below 60% in critical paths means you’re flying blind during refactoring. And if your test suite takes hours, forget about fast iteration — tests should enable speed, not slow it down.
Incidents and error rates: Track severity, frequency, and — critically — which components cause the most incidents. In one of the companies I worked at, this data alone revealed that 80% of our production issues came from 15% of the codebase. That’s where we focused first.
Team velocity trends: A gradual decline in velocity despite a stable team size is one of the clearest signals of compounding technical debt. Don’t just track it — make it visible to stakeholders who control the budget.
The goal isn’t to create a dashboard for the sake of dashboards. It’s to build a case. When you can show leadership that a specific component causes 40% of your incidents and slows feature delivery by 3x, the conversation shifts from “can we afford to modernize?” to “can we afford not to?”
Why Rewrites Still Fail
The rewrite pitch is seductive: throw away the mess, start clean, do it right this time. I’ve seen it proposed at every company I’ve worked at. I’ve also seen it fail spectacularly.
A mid-sized company I consulted with decided to rewrite their customer management system — a core product that had grown unwieldy over ten years. The plan was eighteen months. It took three years. During that time, the old system still needed maintenance, splitting the team’s focus and budget. By the time the rewrite shipped, business requirements had evolved so much that the new system already felt outdated.
The fundamental problem with rewrites hasn’t changed: you’re recreating every feature, every edge case, every hard-won piece of domain knowledge — while the world keeps moving. What has changed is that the alternative — incremental evolution — is now significantly more powerful thanks to AI tooling and platform engineering practices.
Here’s the question I always ask teams proposing a rewrite: If the same engineers who built the legacy system are building the replacement, what exactly will be different this time? Unless the answer involves fundamentally better practices and skills, you’re just creating next decade’s legacy system.
The Platform Engineering Angle
Here’s something that wasn’t part of the conversation in 2019: platform engineering has emerged as one of the most effective ways to evolve legacy systems at scale.
Instead of asking each team to independently modernize their corner of the codebase, you build paved paths — standardized, well-supported ways of doing things that make the modern approach easier than the legacy approach.
At AutoScout24, this means our platform team provides golden paths for compute (Kubernetes-based), observability, CI/CD, and security. When a team needs to modernize a service, they don’t start from scratch. They migrate to the paved path, which comes with built-in best practices, automated compliance checks, and operational support.
The magic of this approach is that it makes evolution the path of least resistance. Teams aren’t “paying down tech debt” — they’re adopting a better developer experience. The framing matters enormously for organizational buy-in.
Practically, this looks like:
Internal Developer Platforms (IDPs) that abstract infrastructure complexity. Tools like Backstage or Port provide a unified interface where teams can provision environments, deploy services, and access documentation — without needing to understand the underlying infrastructure evolution happening beneath them.
Standardized templates and scaffolding. When spinning up a new service (or migrating an old one), teams start from a template that already includes logging, monitoring, CI/CD configuration, and security baselines. This eliminates an entire category of infrastructure debt.
Automated compliance and governance. Instead of manual reviews and checklists, policy-as-code tools (OPA, Kyverno) ensure that every deployment meets your standards automatically. Legacy services that can’t pass these checks become visible candidates for modernization.
The platform engineering approach works because it acknowledges a hard truth: you can’t force teams to modernize. But you can make modernization so much easier than the alternative that it becomes the obvious choice.
AI as an Accelerator, Not a Silver Bullet
This is where things have changed the most since 2019 — and even since the early GenAI hype of 2023-2024. AI tooling has matured from “interesting toy” to “genuine force multiplier” for legacy system evolution.
Understanding legacy code. This is where AI shines brightest. Drop a poorly documented module into Claude, Copilot, or Cursor and ask for an explanation of the business logic, data flow, and hidden assumptions. What used to take a new team member weeks of archaeology now takes hours. I’ve seen engineers use AI to reverse-engineer undocumented APIs, map implicit dependencies, and generate documentation that didn’t exist before.
Refactoring at scale. AI-assisted refactoring tools can now handle transformations that would have been prohibitively tedious by hand: extracting interfaces, breaking up god classes, converting callback-heavy code to async/await patterns, even migrating between frameworks. The key is treating AI output as a first draft — always review, always test. But that first draft saves enormous time.
Test generation for untested code. This is arguably the highest-leverage use of AI in legacy modernization. Michael Feathers defined legacy code as code without tests, and he was right — you can’t safely refactor what you can’t test. AI can now generate meaningful test suites for existing code, covering happy paths, edge cases, and error scenarios. These generated tests aren’t perfect, but they provide a safety net that didn’t exist before, making subsequent refactoring dramatically less risky.
Migration assistance. Moving from one framework, language version, or API to another? AI tools can handle a significant portion of the mechanical translation, freeing engineers to focus on the architectural decisions and business logic that actually require human judgment.
But let’s be honest about the limitations. AI doesn’t understand your business domain deeply. It can’t make architectural trade-off decisions. It hallucinates. It can introduce subtle bugs that look correct at first glance. The teams that get the most value from AI in legacy modernization are the ones that use it as a power tool — wielded by skilled engineers who verify everything — not as an autopilot.
Strategies That Actually Work
With the foundation in place — visibility into the problem, a platform to build on, and AI tools to accelerate the work — here are the strategies I’ve seen succeed repeatedly.
Start With Tests
I keep coming back to this because it’s consistently the highest-ROI first move. Before you refactor anything, write tests for the existing behavior. Use AI to generate the initial test suite, then refine it manually. Once you have tests, every subsequent change becomes safer and faster.
The approach is straightforward:
- Cover critical user journeys with end-to-end tests first. These are your safety net during any refactoring.
- Add unit tests to modules you plan to change. Apply TDD to all new code.
- Use mutation testing tools to verify your tests actually catch bugs, not just achieve coverage numbers.
Strangler Fig Pattern
Wrap legacy components behind clean interfaces. Route traffic gradually from old to new. Kill the old component when traffic hits zero. This pattern works beautifully with platform engineering — the new services live on your paved paths while the legacy components run on their existing infrastructure. Over time, the legacy surface area shrinks until there’s nothing left to strangle.
Modular Replacement
Not every component needs gentle evolution. Sometimes a module is so outdated — built on a deprecated framework, running an unsupported language version, or simply incomprehensible — that targeted replacement is the right call. The key is targeted: replace one module at a time, not the entire system.
Identify candidates by looking at: incident frequency, maintenance cost, developer pain (survey your teams — they know which parts of the codebase make them dread Monday mornings), and business criticality of upcoming changes that touch that module.
Replatforming
Moving from on-premises to cloud, from VMs to containers, from manual deployments to GitOps — these infrastructure-level modernizations can extend a legacy system’s life by years without touching the application code. Start with non-critical services, prove the pattern, then migrate systematically.
At AutoScout24, containerizing legacy services and moving them onto our Kubernetes-based platform gave teams access to modern observability, auto-scaling, and deployment tooling — even before they touched a line of application code. The operational improvements alone justified the effort.
Building the Skills to Sustain It
Tools and strategies are only half the equation. The other half is people.
Why does technical debt accumulate in the first place? Partially because of business pressure, yes. But also because engineers — myself included — don’t always apply what we know. We’ve had decades of accumulated wisdom about clean code, SOLID principles, testing strategies, and system design. The Design Patterns book is over thirty years old. Yet I still see codebases where basic separation of concerns doesn’t exist.
The gap isn’t knowledge — it’s habit. You can name every SOLID principle and still write a 2,000-line controller class because that’s how the existing code looks, and matching the pattern feels easier than fighting it.
Breaking this cycle requires deliberate practice: code reviews that actually discuss design (not just syntax), pair programming on refactoring efforts, internal tech talks where teams share their modernization wins and failures, and — critically — time explicitly allocated for improvement. If every sprint is 100% feature work, technical debt will always grow.
The AI tooling shift makes this even more important, not less. When AI can generate code faster than humans can review it, the ability to evaluate, critique, and improve code becomes the differentiating skill. Engineers who deeply understand design principles will leverage AI to move faster. Engineers who don’t will just produce bad code faster.
The Evolutionary Playbook
If I were starting a legacy system modernization today, here’s the sequence I’d follow:
Month 1: Measure. Instrument everything. Get DORA metrics, incident data, code quality scores, and team velocity on a dashboard that leadership can see. Build the case with data.
Months 2-3: Foundation. Establish your platform — whether that’s a full IDP or just standardized CI/CD pipelines and deployment templates. Define your paved paths. Make the “right way” the easy way.
Months 3-6: Test and strangle. Use AI to generate test suites for critical components. Begin strangler fig migrations for the highest-pain modules. Celebrate early wins publicly — momentum matters.
Ongoing: Incremental evolution. Apply the Boy Scout Rule at scale: every change leaves the codebase better than it was found. New features go on paved paths. Legacy components migrate when they need significant changes. The system gets younger over time, not older.
This isn’t a six-month project. It’s a permanent shift in how your organization builds and maintains software. The companies that get this right don’t have a “modernization initiative” — they have an engineering culture where evolution is continuous.
A legacy system is a business asset that’s accumulated technical debt. It’s not a failure — it’s evidence that your software survived long enough to matter. The question isn’t whether to evolve it, but how deliberately you choose to do so.