Technical Debt — Start Here
There are entire communities, podcasts, and books dedicated to the practice of mending existing code. There is actually so much content and different opinions that it can be confusing. When I started looking into the best practices behind working with legacy code, all the content I found was either too elaborate and failed to deliver a coherent context quickly, or it oversimplified transforming legacy systems into simple technical processes. This article is my attempt at providing a richer and broader introduction to legacy code beyond its technical stereotype.
An Explicit Problem
Any successful organization has reached a level of success through building innovative products and services for its customers. Even with the most successful and well-designed products, technical debt eventually accumulates. Technical debt is a by-product of delivering working software.
Technical debt becomes a problem when you can no longer cheaply change your software to meet the changing requirements of your customers.
As we build our system, if we do not address the accumulation of technical debt, the velocity of our teams eventually comes to a halt. Requirements are not static. It is not guaranteed that what made customers delighted yesterday will still delight them tomorrow.
An Explicit Goal
We want our systems to be resilient and to be resilient they must be able to adapt to an ever-changing landscape. Our goal when working with legacy code is to:
Turn systems that gradually degrade over time into systems that gradually improve.
- Robert C. Martin, Foreword of Working Effectively with Legacy Code
What is Legacy Code?
The term legacy code is loaded with significance in the world of programming. The ambiguity of legacy code can be hard to grasp. Micheal Feathers, the author of (Working Effectively with Legacy Code), spends two full pages defining its meaning. He ends the explanation with a working definition of Legacy Code:
Legacy code is code without tests.
- Micheal Feathers, Preface of Working Effectively with Legacy Code
The main argument is code that is hard to change is code with undefined behavior. Thus, code without tests is code without clearly defined behavior. There is no way for a developer making a change to an existing system to know if he has caused something to break without having tests in place. The main purpose of tests is to allow developers to obtain feedback on the impact of the changes they have made. Tests are used to detect change (regressions) and not to demonstrate correctness.
Though a useful definition, Micheal Feather’s working definition remains a simplification. I personally enjoy Andrea Goulet’s more sensitive definition:
Legacy code is code without sufficient communication artifacts to explain its intent.
- Andrea Goulet, from Legacy Code Rocks!
The essence of Feathers’ definition still resonates here, tests are communication artifacts. However, communication artifacts can go beyond the rigid definition tests and include any form of documentation that defines the intent of code.
Another important factor to legacy code is its emotional aspect. Peter Morlion underscores the emotional response to legacy code by defining it as:
Legacy code is code that you are afraid to change.
All three of these definitions bring their own colors to what we consider to be legacy code. The one component that remains to be mentioned is the importance of legacy code. In practice, legacy systems are the most critical systems we have.
Legacy code has been tried and proven to deliver value to customers.
Sure, untested and useless code does exist… but useless code is sent to die not refactored. The systems worth refactoring are the ones that we know have an impact on our customers. For code to be defined as legacy, the cost of refactoring it outweighs the cost of rewriting it from scratch.
Legacy code is code we juge worth changing to meet new requirements.
Working with Legacy Code
A simple process?
Micheal Feathers enumerates 5 steps to change a legacy system (Working Effectively with Legacy System), these steps are the following:
- Identify the change points
- Find the tests points
- Break dependencies
- Write tests
- Make changes and refactor
Without the context of the first chapters of Micheals Feathers’ book, we could mistakenly believe that changing legacy code is simply a matter of continuously iterating through this algorithm. Though these points are critical, changing a legacy system involves much more than applying a change algorithm.
I wished that a clear-cut formula could be the solution to working with a legacy system, but the reality is much more complex. Issues in software development are not simply technical, they are sociotechnical (see Paul Osman’s brilliant article). An important mentor once told me:
The only requirement for a system to change is to have the people building the system to change.
We should be inspired by Micheal Feather’s change algorithm, but even more by the team behind the system.
Principles over Algorithms
From experience, there are some principles that I try to keep in mind when working with legacy code. They are not rules, but general guidelines that help create enduring change.
We should avoid making changes without a proper minimal test harness. Without a proper test harness, there is no way in knowing if something has been broken by a change to the code base. It might be the hardest step in the process, but it only gets better from there. The test harness must not be all-consuming, but rather the minimal safety net required for a developer to feel ready to change the system.
We should get everyone involved with the legacy system to raise potential problems and define the expected behavior. The culture of an organization is the number differentiator between high and low performers (Accelerate). End-users above all should have their concerns voiced and listened to.
We should not believe in big-bang solutions, but rather aim to make the system better than yesterday. Teams and the systems they create are very different from one another. As well, we can’t simply hope that the same formula can be applied from one project to another. Small discrete improvements are the shortest path to creating systems that gradually improve rather than deteriorate over time.
Start with Empathy
Legacy systems are the systems that got our businesses to where they are today. They are the ones that made our customers delighted and paid our bills. Legacy code deserves admiration and not frowns. It is a rare sight in Tech to actually have a system that stands the test of time. When working with Legacy Code, we must try to focus on the opportunity that lies in making the system able to adapt to change.
I want to make our tried and tested codebases ready to meet the current and future needs of our customers. That being said, I understand the frustrations that can arise from working with difficult-to-change complex and chaotic systems. I believe that programming and more generally building geospatial products should be rewarding and enjoyable work.
People are not designed to serve our systems. Our systems are designed to serve people. Great products are built by great teams working in a psychologically safe environment and not by a single individual (Accelerate). We must empower our teams to collaborate in transforming our sociotechnical systems.