How To Safely Evolve Legacy Code

We’ve all had that code. The legacy code that is so unknown and seemingly so complex that we don’t want to even look at it the wrong way for fear we’ll break something. As much as you want to burn it to the ground, you know the business depends on it too much to do that. 

JB Rainsberger said it best: “legacy code is valuable code we’re afraid to change.”

So when you need to make changes, what do you do? Think about ways to evolve it to get it where you want it to be. 

Characteristics of Legacy Code

“Legacy code is code without tests.” — Michael Feathers

It’s hard to have confidence that everything is working if there aren’t tests to back it up. Typically, key characteristics of legacy code include: 

  • Code that’s hard to understand
  • When changing code it results in breaking the app in unexpected ways
  • Long cycle times to add or change functionality
  • Seeking third-party reviews of the code 

Options For Changing Legacy Code

There are three main options you have when needing to make updates to legacy code —all with varying levels of risk and effectiveness. 

  1. Edit and Pray: You make changes blindly and pray to the coding gods that you didn’t break anything in the process. (Not great, Bob!)
  2. The Great Rewrite: You take extensive time to rewrite the whole thing and try not to make the same mistakes along the way. Typically, this turns into a version of edit and pray as well because it’s really hard to know everything about the system. 
  3. Cover and modify: You do what needs to be done as safely as possible and cover your tracks with tests. 

With cover and modify, you can take an evolutionary approach to code updates, which tends to provide the highest chance for success with the least risk. So, let’s explore how you can approach your legacy code updates in this way.

When You Need To Make Changes Today

When time is critical and you need to get a fix or update out there ASAP, you have a few options. First, you’ll look to find seams into the existing code. From there, you’ll make minor changes using either Sprout or Wrap techniques. 

Sprout vs Wrap

With Sprout, you’ll create a new class or function for the new behavior. Be sure to test drive that new behavior in isolation from the rest of the app, and then insert (or sprout) the new, tested code into your existing code. With Sprout, you’ll leave the existing code untested because you don’t have time to put tests around it.  

The Wrap technique works if you need to add new code to the beginning or the end of a function. You’ll first extract all existing code into another function. Then, call the extracted function from the original function. Finally, you can insert new test-driven developed code before or after the extracted function. 

These two techniques are very similar. The biggest difference is whether you will be adding the new code to the beginning, end or middle of the existing code. 

Tips To Keep In Mind When Needing To Make Changes Quickly

We know, we know… it can be hard to leave bad code as is. But with any might you can muster, do your best to draw a line in the sand and avoid adding more lines of code to the mess. Stay focused on the task at hand and save other areas for another day.

We also suggest that you don’t refactor without tests in place. You’ll just be contributing to the problem blindly and could introduce new issues and headaches that escalate things further. 

When You Actually Have Time To Update Legacy Code

We know it’s rare, but sometimes, you may actually find yourself with time to make updates to the legacy code. When this is the case, we still suggest avoiding the great rewrite. It’s very expensive — especially without a good understanding of the existing system — and delays new feature development and delivering value to end users. 

Here are the steps we’d recommend for safely evolving legacy code. 

  1. Get the ecosystem stable
  2. Understand what the app does
  3. Add characterization/integration tests around existing code
  4. Refactor the code

1. Get the ecosystem stable

Before you start thinking about any refactoring, there are some housekeeping things you’ll want to do beginning with getting the ecosystem stable. Think about things like: 

  • Making sure the code is in source control
  • Make sure the code compiles
  • Having a reliable CI/CD pipeline

2. Understand the app

Before you can effectively rearchitect, you need to understand the current state of things. What can you add to help you get a clearer picture of how things are functioning today? Some of the things that we think about adding are logging, metrics and instrumentation tools to help us gain insights into things like response times and queries.

A few other ways you could work to understand the current state of the application is by reading the code, taking notes and drawing diagrams or refactoring without tests to simply understand how things work. The big thing here is to be sure to throw all of that refactored code away when you are done. This is simply a learning exercise for now. 

3. Add characterization tests

Once you have a better understanding of how things work, you’re in a good place where you can begin to add tests. This will give you a safety net as you begin refactoring.

4. Refactor the code

Now that you have tests in place, you can begin to safely refactor the code. Make changes, run tests, repeat. A good practice is to avoid staying in “long red” — or a broken state for an extended period of time. Our best guidance is to get back to green as quickly as possible. 

The first three steps of this process can be done in parallel across the team, but all of them need to be done first before you can move onto the last step.

By following this process, you’ll be able to evolve and improve your code in a low-risk way. 

Ready to dive into this topic further?

We recommend checking out “Working Effectively With Legacy Code.” It’s a great resource and helped inform many of the ideas discussed here. 

This blog originated from a Lean BYTES presentation given by Lean TECHniques Director of Engineering, Scott Sauber. Lean BYTES are short, 16-minute webinars where you can get the quick hits on a variety of development and IT-related topics. See what’s coming up next and get registered here.