Solving the Problem of Legacy Code
I've been reading lately about the trouble the IRS is having updating its legacy code to handle current changes in tax law. Many of us who have been responsible for maintaining and modernizing old codebases aren't surprised to learn that such a large and complex organization has accumulated a software maintenance burden over the years. But we might find it hard to guess just how bad this one is: reportedly, it involves millions of lines of assembly language code which have been in service for more than 50 years.
In a way, it's encouraging that they've managed to keep such critical infrastructure running on such an old platform. It suggests that just about any platform can be supported indefinitely if need be. In fact, the most common definition of legacy code, which is code written for a platform that's no longer supported, might not represent a problem in and of itself, for any business with enough resources to invest in continued maintenance.
But in fact, the IRS is under enormous pressure to modernize its code, and most of the articles on the subject touch on the real reason when they mention the effort devoted to analyzing the existing code to figure out what it does. You'd think that since United States tax code exists as a detailed publication, there'd be an independent source to consult for questions about the software's required behavior. But as many businesses have famously learned when trying to rewrite large codebases, it's easy to vastly underestimate the number of business procedures that evolve in tandem with a codebase over the years, and the quantity of solutions to subtle problems that it accumulates.
All these accumulated nuances are absolurely required in any potential replacement version of the software, if it's going to work without thoroughly disrupting the business. But they are rarely documented anywhere, and no one remembers them in detail for very long; certainly not for decades. In addition to business-facing behavior, this also applies to internal elements of the codebase which have interfaces and side effects that other parts of the code implicitly depend on. In order for any part of the code to change, a whole lot of these requirements will have to be rediscovered, through some combination of up-front analysis and trial-and-error, and these processes almost always take longer than expected. That's why software engineers associate the term "legacy code" with unpleasant problem-solving experiences, and why businesses associate it with high maintenance costs.
Fortunately, you're probably not the IRS; your codebase may not be as ancient and tangled, and you may not be facing an existential overhaul on the level of a major tax code change. At least, not yet. For the typical software change you need to make which is smaller than that, and which is confined to a limited area of functionality, invest a little more of your engineers' time up-front to carefully review the existing code that will be affected. Have them spend even more time collaborating with domain experts and product owners to thoroughly understand the effects of changes on all business processes that might be affected. This will help mitigate the risks associated with an aging codebase, and result in fewer last-minute delays of work that trips over hidden requirements late in the process.
And each time you rediscover some of the implicit requirements during work on the code, find a way to make them explicit, so they remain visible to anyone who needs to change the code later. That's another advantage you have over the IRS: you're probably using a language that's more expressive than assembly language, and you're definitely working in an era when tools and techniques for automated testing are widely available. One of the many benefits of automated tests, once your team has some experience writing them expressively, is that they form a built-in specification for your code which engineers can read and understand later.
If it's going to take your organization a while to get to the point of writing extensive automated tests for both business-facing behavior and internal interfaces, you can still fight the war against legacy code by storing requirements in some written specification that exists outside the codebase. But be aware that your war will have two fronts: you'll be working to ensure not only that the ever-changing requirements stay documented, but also that the gap between the software's current behavior and the specification as-written, while unavoidable, stays as small as possible. Automated tests avoid this problem, because they form a specification which the code can't possibly fail to satisfy, as long as you require that the tests pass when making changes.
You'll have to experiment to find the best solution for your organization and your codebase, and the best way to keep it in place among all your other priorities. There's more than one way to arrive at a process that makes your codebase slightly better, not worse, with each interaction. But once you reach that point, you'll be able to rely on your software as more of an asset than a liability, even after years of development. You will have turned legacy code into the best kind of engineering problem: one that can be left alone to solve itself.
If you have an especially big problem with legacy code, or any reason that these solutions won't help, leave a comment and let me know.