Modernise Legacy Apps Without a Full Rewrite

A legacy codebase needs an assessment before choosing between replacement and incremental modernisation. This article explores staged changes and the risks that still need to be managed.

Why Full Rewrites Fail

The legacy system has years of bug fixes, edge case handling, and domain knowledge baked in. A rewrite throws all of this away. The original developers have moved on, and nobody fully understands why certain things work the way they do. During the rewrite, someone will inevitably say "let's also redesign the workflow while we're at it" and suddenly you're not just rebuilding the technology — you're redesigning the business process too.

Meanwhile, the old system still needs maintaining, so you're paying for two systems at once. Bug fixes and feature requests don't stop just because you've started a rewrite. Your team is now split between keeping the old system running and building the new one, and neither gets enough attention.

A rewrite can uncover undocumented business rules and integration dependencies. Include discovery and contingency in the estimate, and compare the cost and risk of replacement with incremental changes.

Incremental modernisation lets teams validate changes in smaller stages. Big-bang rewrites can introduce risks around scope, delivery dates, and budget.

The Strangler Fig Pattern

Named after tropical fig trees that gradually envelop their host, this pattern replaces legacy components one at a time. New functionality is built in the modern stack. Old functionality is migrated piece by piece. A routing layer directs traffic to either the old or new system depending on which component has been migrated. Eventually, the old system has nothing left and can be switched off.

Incremental routing can support rollback while the old implementation remains compatible. Shared data, irreversible schema changes and decommissioned components can prevent a simple reversal. Define and test the recovery path for each stage; see Microsoft's Strangler Fig guidance for coexistence and data-migration considerations.

Possible migration scenarios include Web Forms to Razor Pages, desktop applications to web applications, and replacing selected legacy services. These are examples, not accounts of commissioned projects. Check that requests can be routed between implementations and that their data remains compatible.

Where to Start

Don't start with the most complex component. Start with something small, well-understood, and low-risk. A settings page. A simple report. A lookup API. This lets you establish the new architecture, set up CI/CD, configure the hosting environment, and prove the approach works — all without risking anything critical.

The first component you migrate is actually the hardest, not because of the code itself, but because you're establishing the entire new infrastructure at the same time. You need to set up the new project structure, configure build pipelines, establish deployment processes, and get the routing layer working. Once this foundation is in place, subsequent migrations are significantly faster because the infrastructure already exists.

After the first component, prioritise by business value and risk. Components that change frequently are good candidates because you'll get the most benefit from modern tooling. Components with security vulnerabilities that are hard to patch in the old framework should be prioritised for safety. Components that are causing performance problems are often dramatically improved by modern .NET's performance characteristics.

Avoid the temptation to migrate components that nobody uses or rarely changes. The point of incremental modernisation is to deliver value continuously. Migrating a report that runs once a quarter delivers less value than migrating the API that handles every user interaction.

The API Boundary

Put an API layer between the old and new systems. The new frontend calls the API. The API talks to both old and new backends depending on which component has been migrated. This gives you a clean separation point and lets you migrate at whatever pace works for your team.

The API boundary also provides an integration point for testing. You can write integration tests against the API that verify the same inputs produce the same outputs regardless of whether they're handled by the old or new system. This automated verification gives you confidence that each migration step preserves existing behaviour.

In practice, I typically implement the API boundary as a thin ASP.NET Core Web API project that sits in front of both systems. For migrated components, it routes requests to the new code directly. For components that haven't been migrated yet, it proxies requests to the old system. The routing logic can be as simple as a feature flag per endpoint — flip the flag when the new component is ready, flip it back if there's a problem.

This pattern also enables gradual rollout. Instead of switching all traffic to the new component at once, you can route a percentage of requests to the new system and monitor for errors before increasing the proportion. This is particularly valuable for high-traffic components where the risk of a subtle bug affecting many users is highest.

Dealing With the Database

The database is usually the hardest part of any modernisation project. Legacy systems often have tightly coupled stored procedures, triggers, views, and direct table access from multiple applications. Changing the database schema risks breaking systems you didn't even know depended on it.

My approach is to keep the existing database initially and build new services that read from it. This means the old and new systems share the same data without needing data migration or synchronisation. Gradually introduce new tables or schemas for migrated components that need different data structures. Use database views to provide backwards compatibility — the old system continues reading from what it thinks are tables, but they're actually views on the new schema.

For stored procedures, I migrate them to application code as part of the modernisation. Stored procedures are difficult to test, difficult to version control, and create tight coupling between the application and the database. Moving business logic into the application layer (where it belongs) makes the system more testable, more portable, and easier to understand.

Once the old system is retired, review unused database objects and compatibility layers before removing them. Data changes need their own migration and recovery plan, even when application changes are incremental.

Measuring Progress

Track what percentage of traffic goes through the new system versus the old. This gives you a clear, objective measure of migration progress that stakeholders can understand. Set realistic milestones — 25, 50, 75 percent — and celebrate each one.

Also track the more subjective measures: how long it takes to implement a new feature in the migrated components versus the legacy components, how many production incidents originate from each system, and how developer satisfaction changes over time. These metrics help justify continued investment in modernisation when stakeholders question whether the effort is worthwhile.

I maintain a migration tracker document for every project — a simple spreadsheet listing every component, its current status (legacy, in-progress, migrated), the estimated effort to migrate, and any dependencies or blockers. This gives the project manager visibility into what's been done, what's in flight, and what's still ahead. It also helps with planning: if a component has dependencies on two others that haven't been migrated yet, you know to schedule those first.

When to Stop

Not every component needs migrating. If a legacy component is stable, rarely changes, and doesn't cause operational problems, the cost of migrating it may exceed the benefit. It's perfectly acceptable to have a modern system that still proxies a handful of requests to a legacy backend for rarely-used functionality. The goal is to eliminate risk and improve developer productivity, not to achieve architectural purity.

Compare the effort of migrating rarely used screens with the benefit to their users. Prioritise changes using evidence about usage, support needs and risk.

Need Help?

I specialise in incremental legacy modernisation for .NET applications. If you're facing a legacy system that's holding your team back and want to explore modernisation options, get in touch for a free 30-minute discovery call. I'll give you an honest assessment of what's involved and whether the strangler fig approach is right for your situation.

Need Help With This?

I offer consulting and hands-on development for .NET, Azure, and DevOps projects. Let's talk about how I can help.

Get in Touch →