As a software consultant, I have worked on everything from quickly moving greenfield projects to deeply coupled legacy systems with many obstacles to even the smallest of changes. I found some interesting challenges arise when trying to apply XP principles to legacy software projects. Part of that challenge is reconciling the approaches and motivations between XP and legacy software teams. I wanted to take this opportunity to highlight the qualities and behaviors I’ve noticed that make these engagements successful and which ones create additional roadblocks. Generally, attributes I see as having a high impact include taking an incremental approach, leveraging existing value, and team involvement/allocation from both sides. To frame how each of these contributes, let’s start by defining what we mean by “legacy software.”
What is Legacy Software?
While the internet yields many answers to this question, I will narrow our scope to include codebases that exhibit some combination of these characteristics, which should cover most cases:
🎁 code inherited from someone else, possibly with little or no documentation
🛑 hard to change (may receive infrequent feature updates as a result)
🗓️ contains outdated/obsolete tech stacks and/or patterns
⁇ untested code (as defined by Michael Feathers in Working Effectively with Legacy Code)
✅ code that works (has supported the business so far)
Landing on one of these projects as a consultant, one immediate challenge is establishing the context and then a roadmap to getting the first new feature or update into production. While it can feel overwhelming, taking an empathetic, proactive mindset to the task is the right first step.
Where To Start
For many legacy systems, there may be large overhauls and/or architectural changes needed to get the ball rolling. It might not be as simple as just picking a place to start and then writing code. While there are a number of strategies for these situations, and that can be a whole topic on its own, let's focus on a common case I see where there is at least a place to start working in the code.
Ideally, as an XP practitioner, I try to find where I can use TDD to work incrementally and de-risk new code. Realistically, it isn’t always easy to add tests to a legacy system due to factors like language, architecture, patterns, etc. In some of these cases, tests are either very hard to introduce or actually add complexity. To address these situations, there are many techniques for incrementally adding code and breaking dependencies/coupling. For now, let's consider a codebase where we can reasonably add tests.
Given that we have a testable codebase, we can approach it incrementally.
A test-driven approach to modifying legacy code
- First, we collect business rules and write tests against them for the existing code, asserting the current behavior and creating a baseline.
- Once we have a passing test suite, we refactor the code as needed with our tests, giving us confidence that we aren’t changing behavior.
- Finally, we can write new tests for and implement our new feature.
Incremental Value
In this way we derive incremental value throughout the whole process. Not only are we improving one component at a time, but we are increasing our confidence at every step. First, we add confidence and speed to our future development processes when we add tests. We enable development speed and reduce risk by refactoring code. Ultimately, we drive real business value by adding a new feature. This is not a comprehensive list of benefits at each step, but it is enough to drive the point home. If we stopped after any one of those steps, we have already made things better.
We still may not see all the value tests can give us until we find a way to get them executed in a way that gates deployment through CI/CD (if not already) and adopted as a practice-level norm for teams working on the codebase, but it still gives us confidence as we refactor.
Leveraging Value in Legacy Code
There are some realities about legacy code that are often forgotten. Firstly, the business needs were probably met using the most effective tech stack and software patterns available at the time. The engineers who built it probably did the best they could with the tools available to them. This understanding should form the basis of our interactions with legacy software teams. In addition to empathy, taking a proactive approach to legacy codebases is essential to find momentum.
Another reality is that these systems have worked for the business so far, maybe for a long time. It has likely supported thousands, if not millions, of users over the years and has proven that it works. Instead of assuming that it needs to be fixed because it doesn’t work, we can start by thinking of how to make it work better. Oftentimes, the value in modernizing legacy code isn’t in replacing it with something we think is more valuable but instead in finding untapped potential value in the current system.
Roles of Teams and Stakeholders
Responsibility for success is a shared effort. If an organization wants to buy modernization off-the-shelf, it must also be willing to accept a level of risk and possibly reduced quality of outcome. Success is more likely when the engagement is treated as a truly collaborative effort with engineers from both sides working together as one team.
Without this collaboration, tension often exists between teams. Operating in a silo, consultants may struggle to get context or know where to make changes. Not knowing what they don’t know may slow down efforts and make it harder to ask the right questions of the right people. Legacy system engineers may also struggle to see value in proposed changes as they look incomplete or appear ignorant of some internal knowledge about the system or bespoke business constraints.
Additionally, code written in a silo by consultants will be harder to adopt and maintain as they roll off, leaving internal engineers with 2 options:
-
Put in extra time/work to try to understand these changes in code/patterns later when another change needs to be made.
-
Fall back to familiar patterns, perhaps under deadline pressure.
Tension between consultant and legacy teams working in separate silos
Change Management
IT is a fast-moving field. For many teams, it’s common to see new frameworks or libraries emerge, rise immensely in popularity, and then eventually fall out of favor for the next shiny thing- all while the code the team has maintained over the years continues to work without much change. It’s only human to want to resist some change and to be skeptical of its value. Being part of something that has supported your business for so long can increase these feelings. This is another area where collaborating incrementally really shines. It allows us to communicate and demonstrate value-adding change gradually, building trust and assuaging the fears around that change. Not only does this help manage the change non-technical stakeholders have to make, but it also helps with contributors directly involved with the software.
When a company brings in consultants with little context or communication, it can be unsettling for engineers on the project. However, when a company shows its commitment to leveraging the skills of its existing teams working on legacy software, it validates their abilities and acknowledges that their contributions continue to be of high value to the company. It keeps momentum high, and it helps break down the tension between the legacy team and consultants. Working incrementally through changes together, the modernization path is clear before you instead of appearing as a distant island with no idea how to get there. I can’t over-emphasize how much making decisions together leads to both a positive relationship and a better, sustainable outcome. It truly is night and day, and it starts with business leaders committing to involving their own teams in the effort, day-to-day.
Stakeholders with proper influence can greatly help the effort by making sure that internal engineers are available to be part of the modernization effort, pairing with consultants to create shared ownership in new code. Leveraging the strengths of both teams alleviates tension, leads to higher-quality code, and de-risks the outcome of the engagement. By working this way, we can roll off the engagement as consultants and, thanks to our tight collaboration and well-tested code, leave our clients with code they can easily continue to build on.
Key Takeaways for Successful Modernization
Legacy systems often have a tremendous amount remaining/unrealized value and being able to successfully find and tap that value depends on everyone involved. While I covered some high-level characteristics of engaging on these projects, generally following this approach helps avoid some common pitfalls that leave high hopes for modernization feeling stagnated or unsuccessful. By bringing the right mindset, approaching the problem incrementally, and sharing ownership of the outcome, we can prolong the life of legacy software and the value it provides.