Legacy systems
Modernizing a legacy .NET or Java system without stopping the business
Why a full rewrite is almost always a gamble, and how to move step by step while protecting what works today.
Almost every organization has an application that "holds everything together": written ten or fifteen years ago in .NET Framework or Java, changed by many hands, poorly documented and feared by anyone who has to touch it. It works, and that is exactly why it is hard to change. In many years of work on banking, insurance and public sector platforms I have learned that modernizing does not mean throwing things away, but evolving without interrupting the service.
Why legacy software sticks around
A legacy system is not just "old software": it is software that holds years of business rules, exceptions, fixes and knowledge that is often written down nowhere else. That is why replacing it is harder than it looks.
The signs that it is time to act are fairly easy to recognize:
- runtimes, frameworks or libraries that are out of support or no longer updated, with known vulnerabilities;
- manual, slow and risky releases, done as rarely as possible;
- no automated tests: every change might break something elsewhere;
- difficulty finding people willing and able to work on it;
- increasingly complex integrations with modern services, APIs and the cloud.
The trouble with a full rewrite
The temptation is to start from scratch with new technology. Sometimes that is the right call, but more often the "big bang" brings three risks:
- hidden rules: the new system has to replicate behaviors nobody remembers until they are missing;
- double cost: for months you must keep the old system running while building the new one;
- moving target: while you rewrite, the business keeps asking for changes, which have to be made twice.
That is why I prefer an incremental approach, where every step delivers value and can be verified.
A six-phase path
1. Assess what exists
Before touching the code you need a map: modules, dependencies, framework and library versions, databases, scheduled jobs, inbound and outbound integrations. Above all, you need to understand where the problems concentrate: which parts change often, which cause the most incidents, which are business-critical.
2. A safety net
The first investment is not in new features but in the ability to change without fear:
- code under version control and a repeatable build, ideally in a CI pipeline;
- characterization tests that capture current behavior – even when it is questionable – so you notice immediately if a change alters it;
- enough logging and monitoring to understand what happens in production.
3. Stabilization
Security updates, removal of abandoned dependencies, fixes for recurring defects and documentation of the critical parts. It is the least glamorous phase, but often the one that cuts incidents and maintenance costs the most.
4. Strangler pattern: replace piece by piece
The strangler fig pattern puts a facade – typically an API gateway or reverse proxy – in front of the existing system and gradually moves functionality to new components. Users and connected systems do not notice the change: requests are routed to old or new code depending on what has already been migrated. New features are born directly in the modern components.
5. Platform upgrades
Some concrete pointers for the two most common enterprise ecosystems:
| Area | Typical path |
|---|---|
| .NET Framework → modern .NET | First separate the business logic libraries (portable via .NET Standard or multi-targeting), then the application projects. The .NET Upgrade Assistant helps with the mechanical parts. |
| WCF services | CoreWCF lets you keep existing contracts on modern .NET; for new services, consider REST or gRPC. |
| ASP.NET Web Forms | There is no direct migration path: the UI must be rewritten, preferably section by section behind the same facade. |
| Java | Moves between LTS releases (for example from Java 8 to 17 or 21), paying attention to libraries removed from the JDK and to the namespace change from javax.* to jakarta.* required by Jakarta EE 9+ and Spring Boot 3. |
6. Continuous delivery
Containerization with Docker where it makes sense, CI/CD pipelines with GitHub Actions or Azure DevOps, test environments aligned with production. Releasing often, in small batches, is the safest way to keep the system evolving.
The database: the real core
In many legacy systems the most important logic lives in the database: stored procedures, triggers, views, jobs. SQL Server, Oracle and DB2 have handled huge workloads for decades and there is often no reason to replace them. My advice is never migrate the application and the database at the same time: modernize one layer, stabilize it, then deal with the other if needed.
What about Artificial Intelligence?
Language models are a real help in modernization: they read old code and explain it, draft documentation and tests, and suggest translations between old and new APIs. They remain tools that need expert review, though: responsibility for the system's behavior stays with whoever maintains it.
In short
Modernizing a legacy system is a journey, not a one-off project: safety net, stabilization and piece-by-piece replacement, with the service always running.
If part of the journey also involves automating manual work around the system, you may find the article on how to automate business processes useful.