Most of us who have been responsible for the care and feeding of an enterprise application have had to modify someone else’s code. Whether the modification is due to a newly found bug or to enhance existing functionality, changing someone else’s code is an interesting endeavor.
The first challenge is in just trying to figure out what in the world they were thinking. As a young software developer, I was likely a bit arrogant in judging others’ work. Then one day I ran across code that I had written myself several months earlier. It was a bit humbling having to spend time asking myself, what in the world was I thinking when I wrote this?
As our industry is learning to develop effective and secure applications, we’re finding that involving a broader spectrum of participants in the development process increases quality. That’s the idea behind DevSecOps. New development should include security and operations from the initial design phase.
But what about existing applications? Should we just rewrite existing applications to fit a better development model? In most cases, the answer is probably not all at once. Replacing a behemoth enterprise application takes a thoughtful approach to application refactoring.
Legacy application complexity
Enterprise applications tend to grow to fit their changing environments, and over time they seem to take on a life of their own. Very few organizations have cared to spend the time and effort (or budget) to properly design application software enhancements. The common mantra is “Just make it work, then move on to the next request.”
But at some point older applications that have been patched and extended in dozens of ways begin to show their age. They become “legacy” applications. A legacy application is an older, outdated software application that still gets the job done but doesn’t embrace the latest technology or techniques. You’ll find them in nearly every organization.
Legacy applications are like 10-year-old cars. They are dependable, but they have lots of mileage, they don’t have any of the newest features or gizmos, and you know that repair costs are right around the corner. From an enterprise perspective, legacy applications generally lack many of the security or privacy features to meet compliance requirements and can be difficult to integrate with the latest technology. It’s time to upgrade.
The main problem with upgrading legacy applications is that the organization probably relies on the application to stay in business. A true upgrade means replacing the legacy applications, but you can’t replace an integral part of your business without disrupting the business.
So, the next best option is to update the legacy application instead of replacing it. That means refactoring legacy functionality in a way that maps the latest technology and application development strategies onto the legacy design. That sounds good in theory, but it rarely works out well in practice.
For starters, refactoring legacy code is hard. Legacy applications were developed in a different time and environment. Before DevSecOps, security probably wasn’t a primary design objective, and architectures were likely focused on the client-server model. Internet access was slower and mobile devices weren’t valid endpoints. In short, the world was a different place, and the legacy application just wasn’t designed for today’s infrastructure.
Many legacy applications also were designed before modular design caught on, and the resulting code tends to be monolithic. To make matters worse, years of quick fixes make modified legacy applications unbelievably complex and resistant to the best refactoring intentions. Any attempts to globally refactor risk breaking the delicate application’s ability to run the business. The business risk of a direct refactoring effort is just too great.
There must be another way. Instead of completely replacing or refactoring the entire application, there is a novel incremental pattern that supports the enterprise’s needs to transform the application while maintaining critical business functionality. It is based on a fig vine.
Introducing the strangler pattern
In 2004, Martin Fowler traveled to Australia and encountered a rainforest phenomenon known as the strangler fig. A strangler fig is a vine that starts its life high in trees and extends its roots downward. Strangler fig seeds are dropped into the tops of trees by birds and germinate there. The vines slowly grow toward the ground, enveloping the host tree as it grows. Eventually, the strangler fig surrounds the tree, and in some cases the host tree dies.
Being a software developer who worked with refactoring projects, Fowler mapped the behavior of the strangler fig to a pattern for refactoring large legacy software applications. Instead of just replacing the legacy application, slowly encasing it with new code could solve the problems of modernizing a legacy application and reducing business risk. The old software that runs the business wouldn’t go away; it would just be slowly and methodically replaced with software designed using best practices for the 21st century.
The idea behind applying a strangler pattern to application refactoring depends on building new layers that surround the existing application. By placing new functionality at a higher layer in the application stack than the core legacy application, the DevSecOps team can roll out a completely new application, one service at a time. Eventually, the core legacy application won’t be doing anything and can safely be switched off.
The strangler pattern works so well because it slowly replaces existing legacy functionality. The slow growth makes taking over a behemoth possible — just like the strangler fig vine completely taking over a mighty tree.
Transforming legacy applications
The strangler pattern approach is fundamentally simple. The first step is to establish a proxy between the legacy application and users. In the beginning of the strangler process, the proxy simply passes each user request straight through to the legacy application. Once you have a proxy in place, you can choose to redirect requests to another application component.
For example, suppose you choose to replace the identification and authentication functions first. You would develop new identification and authentication services that are completely separate from the legacy applications. To start using the new code, you would intercept all login attempts, and instead of passing them through to the legacy application, you would call your new functionality. After that, repeat the process with new functionality and direct more and more requests away from the legacy application.
Users should have a seamless experience, albeit one that may provide new and improved functionality as the new codebase expands. Most strangler pattern uses depend on the new functionality using new data repositories. If the new code uses new data, there will be some need to coordinate data with the legacy application. One of the more difficult aspects of applying the strangler pattern is approaching the data partitioning in a strategic manner. Minimizing data redundancy requirements through the refactoring process eases the transition.
In the end, the goal is to slowly reduce reliance on your legacy application’s functionality by replacing monolithic functions with modular services. If planned well, a slow strangler approach can minimize risk for any business. It may take longer than a complete replacement, but the process is less of a shock to the organization.
Strangulation best practices
The strangler pattern has been used by multiple organizations since its introduction. As with any enterprise endeavor, teams learn lessons when evaluating project performance.
Paul Hammant’s blog includes a great article on strangler pattern case studies. At the end of his article he lists a few best practices that have emerged from strangler refactoring projects. I’ve reprinted his suggestions here:
- You really should phase the strangulation. Keep your larger app in a continually deployable state while working on it. The first go live after a month or so of work, then every two weeks after that at least or you’ll fail. That would probably via project cancellation by a checkbook-holding sponsor.
- Do enhancements or new “business value” work concurrently with strangulation, while getting all to agree that both are happening. As you work on the strangulation, a decent percentage of work should be enhancements too. This allows value to be associated with each release from the point of view of the people paying for it. ROI and all that, isn’t just abandonment of costly end-of-life IT choices, it is about tangible changes for the better. From top to bottom, everyone needs to agree that both are happening.
- The additional of integration and functional test suites as a safety net is key. This is particularly true for when the old technology did not have unit test coverage. The functional tests will be able to step between old and new (and back), to prevent surprises.
- Understand that Non functional Requirements (NFRs) that don’t directly cheapen the re-implementation may jeopardize the initiative. Jeopardize in the “courting cancellation” territory again. Various authority figures may have pet technologies to include, or things to exclude. The test is whether the dev team cranking stuff out agrees or not.
- Agile methodologies optimize everything for maximized developer throughput, and phased deliveries to production. You will not manage this with waterfall, unless you want glacially long intervals between production pushes. The Pols/Stevenson white paper drills much further into the Agile aspects.
- Lastly, you should always be aware that there could be functionality and context hidden within the old application that people have forgotten about, and that a team of business analysts assigned to reverse engineering behaviors might also miss. This is a risk for any “rewrite” though.
These best practices come from real experience with refactoring using the strangler pattern. Stick with these and your refactoring project will likely benefit — without causing too much disruption to your business.