Renovating Legacy Software
An Allegory
Imagine that you are the captain of a big rowboat that has been rowing back and forth across the ocean for years hauling cargo. It has paid for itself many times over because of the value delivered by those crossings. The crew are very experienced and know how to keep the boat seaworthy and even make improvements to it. This is becoming increasingly difficult because a relentless focus on short term efficiency has led you to increase crossings at the expense of maintenance. The crew are getting on in years, and it's challenging to recruit new crew members. Candidates fresh out of boat school want to work on boats with new tech like hydrofoils and composite sails and do not view experience on the legacy boat as the basis for a good career.
You describe the situation to the admiralty and they agree that this deserves emergency investment. You propose to build a new boat with all-new technology to replace the old one, and to hire a small team of new boat-school graduates along with some low-cost boatwrights from a foreign land to build it, catch up with the legacy boat, and eventually take over its cargo business.
The admiralty offers enough funding for a crew for three years. Even with the boatwrights it's not as big as the crew rowing the legacy boat, but you reason that they are working with new technology, so they ought to be able to go faster. The legacy boat's crew are tasked with continuing to move cargo across the ocean, and are still making changes to their boat. You reassign one person from the legacy crew to write a document that details everything that the boat does so that the new team can use that as a specification for their boat.
The legacy boat and its experienced team are delivering value today, and must be kept competitive. The smaller inexperienced team has no boat, and their only guide is the specification and whatever they learned in boating school. The specification was one person's idea at one instant of what was important to your business. The legacy boat is still under development and is changing with every passing day.
Quiz
What customer or business problem are we solving?
What other ways might there be to solve the problem?
What are the new crew's chances of getting their boat seaworthy before the admirals pull the funding?
If the new team could build a boat quickly, what are their chances of catching up with the larger experienced crew?
Some observations
We didn't define the customer problem carefully before proposing a solution. Is a boat the best solution?
We jumped to the “build from scratch” approach without exploring other ways to solve the problem
The inexperienced team lacks the context that the experienced team has
The experienced team are busy keeping the business in business
The specification is almost instantly out of date, and there is no way to prove that it captures what is important for your customers
A smaller, inexperienced team starting from scratch is unlikely to overtake the experienced team if your customers have something to say about it
We didn't define the customer problem carefully before proposing a solution
Takeaways
In this Insight, we’ll review some key practices for renovating legacy software successfully.
Define the problem carefully
Create automated test scaffolding around the legacy product instead of writing detailed specs. Use the automated tests as a check on new code
Consider alternative approaches
Engage the legacy team in the renovation effort
Always be integrated. Make the renovation team show capability from as close to day one as possible, and every few weeks thereafter
Always be integrated
Release on a rapid cadence
Do frequent end-to-end demos
A good problem statement is half the solution
What is the problem that we are trying to solve by modernizing or rewriting a valuable software product?
Some candidate issues include:
Cost of Change - adding new capabilities costs too much. Note: Consider the contribution of sub-optimal development practices (like over-reliance on manual testing).
Technical obsolescence of key components or platform.
Difficulty hiring new people to maintain the code (ancient toolchain, business model)
Customer and business fit - desire to move to a SaaS business model or to offer capabilities delivered by new technology (like AI/ML). Eg. Customers want to reduce their cost of ownership with cheaper scaling, browser based UI, central data management and remote administration.
Maintenance cost (security, platform obsolescence or instability, complexity, compliance issues)
What is your problem statement? Ask “why” enough times to be sure you have gotten to the real issue before identifying possible solutions.
Automated testing is your life jacket
The best time to have begun developing automated tests is when you began developing your legacy product. The second best time is now. It almost doesn’t matter what your problem statement turns out to be.
There is no way to test a specification, so invest in test automation before modernizing legacy code. If your legacy product doesn't have automated test coverage, this may be contributing to the problem you want to solve. Automated tests assure that you have captured the critical behaviors of the legacy code. With good automated coverage, defects stay fixed. Test automation reduces release cycle times and build breaks, and guards against unanticipated side-effects when code is changed. I cannot emphasize enough how important it is. Yet many organizations with legacy products have failed to invest in it. Neither of these approaches (spec or test automation) tell you whether you are actually solving a problem your customers care about.
No matter what approach you take to solving the problem you articulate about your legacy product, you will be better off if you have surrounded it with good automated test coverage. It makes sense economically because you pay for manual testing every time you perform it. Automated testing is essentially free to run; you only pay to keep it current and to expand coverage as you find defect leaks. It is also much faster than manual testing for a given level of coverage. You can run it as soon as a change is committed, which improves developer productivity. Any issues that arise are fixed more quickly. Broken builds are rarer so all developers benefit.
Behavioral Evidence
If possible, instrument the legacy system so that you can measure what capabilities are actually being used and how. This evidence informs decisions about what to deliver first, and what can safely be dropped or changed. Here is a starting point: a logfile upload can be part of any support case for on-premise software. If retained, this information can help your product team analyze what parts of the product are most involved in support calls (implying both that they are being used and that they may need some design attention). If you have a SaaS offering that lacks tools to measure how customers use the service, you are missing a huge opportunity to improve your product-market fit and focus effort on things that deliver the most value.
With test automation and behavioral evidence you are in position to begin upgrading your legacy system with confidence that you know what behaviors are important and will have early warning if they are broken.
Consider alternative approaches
With a well-crafted problem statement in hand, consider these alternatives to a cold rewrite. All need support from a good suite of automated tests.
Does your legacy code assume it is running on a dedicated desktop PC? It may be coded to assume that it can expose user interface elements from the desktop OS. This can make a move to the cloud challenging. There are commercial tools that emulate those services and make the native UI available in a browser. This is a quick way to get your venerable code onto a remote server, but it will not directly solve cost of ownership problems. Eventually you will need to rearchitect the code to separate the presentation layer from the underlying logic.
Careful architecture will help reduce risk in moving a legacy desktop or “fat client” application to the cloud. Teasing Windows UI elements out of monolithic code is one of the challenges. You may be able to do this in stages using one of the techniques below. ***
Automated refactoring - you can clean up crufty code using automated refactoring tools offered by modern Integrated Development Environments (IDEs), assuming your toolchain has one.
Break monolithic code into modules by inserting application programming interfaces (APIs) - this is a relatively low-risk approach as long as you have the coverage to test that the system behaves the same way after inserting the interfaces. Write automated tests at the module level as you do this - you may then be able to rehost or rewrite each module independently.
Encapsulate critical code - wrap legacy code in an API and expose as a service. If compatibility is essential and the code is stable. You can refactor it from here if necessary. This approach wraps legacy code in a modern interface layer. To succeed the legacy code must have minimal external dependencies.
Rehost the code on new infrastructure - lift and shift to the cloud for example. This is usually a dead end for monolithic code, but may allow you to learn from customers as a step toward a SaaS offering. Once you have isolated modules with APIs, this can be an effective scaling strategy.
Move to a new runtime platform - this entails breaking dependencies with the old platform, which is careful work.
Rebuild (from scratch, but with the assistance of your automated test suite)
Replace, considering updated needs - you risk leaving your existing customers unable to move to the new system unless you are very careful. Again a good test suite is critical. You can write tests for the updated needs in advance. The test is better than a specification.
Many of our clients face the hard problem: a venerable legacy system that is brittle, has accumulated technical debt, facing technical obsolescence and a shrinking team of people who can maintain it (if there are any left), AND a desire to transition to Cloud hosting, a SaaS business model, or deliver AI/ML capabilities. Proceed with caution - a good safety net of automated tests is an essential starting point in this case. You may be able to do this piecewise rather than attempting to write tests for the entire system.
When updating legacy software, rewriting from scratch is expensive and risky. Using some of the intermediate steps outlined above can significantly reduce that risk and the cost to get where you need to go.
Engage the Legacy Team
Your existing team has domain knowledge and deep experience with the current system. They may lack exposure to new technologies that you need to use, and they may be set in their ways with regard to what has worked well in the past and have a hard time understanding how the market and business needs have changed. Your best shot at success is to have the new kids work with the old hands to mix the new direction and tech with solid experience.
Integrate Early and Often
One of the biggest risks in any development effort is that various elements don’t work together correctly after the development is done. The best way I know of the avert this risk is to require the team to demonstrate integrated capabilities end to end beginnng early in the effort and on a regular and rapid cadence after that. This prevents self-delusion about what needs to be done and as a bonus lets you and your stakeholders see progress toward the vision. This is in start contrast to typical rewrite programs where the team is allowed to run submerged for an extended time before showing up with something that can be validated by customers and stakeholders. The usual result is integration failures, rolling slips, and disappointment. Use a rapid release cadence, continuous integration, and end-to-end demos to take that problem off the board.
Copyright © 2023-2025 John W Sadler Jr - All Rights Reserved