When the term "legacy code" comes up, it's usually said or received with a tinge of disdain. A preliminary Google search for "legacy code memes" brings up hundreds and hundreds of image macros of people tearing their hair out, looking frazzled, or hugely disappointed.
When I started working as a software developer 6 months ago, I had no idea what legacy code was or what working with it entailed.
During my fourth month as a junior developer, I was asked to add a search filter modal to an app built by one of my coworkers two or three years ago. It seemed easy enough; I'd spent the majority of the last four months working on an incredibly complex app for another client involving our standard stack: TypeScript/React/Redux/Redux-Saga. I had already solved many unique problems and felt confident in my coding ability enough to make a simple modal to pass query parameters to the backend.
As you may have guessed, it wasn't nearly that simple. But why?
Legacy code is code inherited from another developer or team which uses older technologies that are no longer supported or have been superceded by a newer version. Many programmers say that "code becomes legacy code as soon as it's written". The functional difference between "regular" code and legacy code could simply be that it has different conventions compared to what you're used to working with.
In my case, the app I was assigned to utilized Flow rather than TypeScript, and it wasn't as strongly typed as I was accustomed to. This made it a bit harder for me to understand the structure of the data that was being fetched from the backend. No types meant that I was running into TypeErrors on runtime much more frequently, which can be difficult to debug when you're writing a large feature. On top of this, the app used a much older version of React which needed to be reconciled with a compatible version of the component library I wanted to use to build the modal.
Before I get into the nitty-gritty of how to handle legacy code with a sense of poise and rationality, I want to add a disclaimer that legacy code isn't all bad and working on a legacy project doesn't have to be terrible. On the contrary, working on legacy code taught me to be flexible, patient, and above all else, the experience allowed me the chance to solve problems with a new perspective in a novel context.
In fact, it made me a better developer than I was before I started working through the aforementioned codebase, and hopefully your legacy project can teach you something too.
In a perfect world, every codebase has a robust README that contains concise explanations of how the project works, code comments that explain the exact logic of the original author, and the whole application makes perfect sense. However, this is rarely the case. Many READMEs don't get updated as projects develop, people forget to write comments, assume that their logic is obvious to a new developer, or they simply run out of time to take care of those things.
If you're lost and don't know where to start, ask yourself these questions:
- What is the app’s purpose?
- How does data flow through the app?
- How does your feature fit into the app?
When you can get a sense of the big picture, it's easier to figure out how best to tackle the problem. Maybe you need to create a new file and create a new component. Maybe you need to write a utility function and test it. Whatever the case may be, understanding the broader context of your problem is a good first step to creating a solution.
Breaking an app temporarily while adding a new feature is an inevitability, no matter what level of developer you are. This is normal and expected, especially if you're new to the job, working in a legacy codebase with an unfamiliar stack, or some combination of the two.
The best way to prevent these breakages from becoming long term problems is to test your app thoroughly with both unit tests and manual tests. Having these tests in place and knowing exactly what kind of coverage you get out of them will save you and future developers a lot of time. In addition, rigorous tests make the app more scalable and also give you a litte dopamine rush every time your tests run clean.
For manual tests, you'll want to develop a test matrix and make sure the document is accessible to future developers. For the matrix, you'll want to define a set of actions, the expected behavior, the actual behavior when you test it, and any other details that are important, like so:
I'll be covering how to implement both types of testing efficiently into your workflow in a future blog post.
Assuming your project was written by a current or former employee at your workplace, someone else probably knows what's going on in the app, or at least knows enough to get you unstuck. Learning to swallow your pride and ask someone else is a an uncomfortable step for some, but a necessary one for growing as a developer, and maybe your coworker can teach you a few new tricks.
A good way to make efficient use of your time (and theirs) is to formulate informed questions. Try going back to looking at the codebase as a whole and figure out the gaps in your understanding. Not only will it help them to get a better sense of what your problem is, but it shows that you took the initiative of trying to solve the problem on your own first.
If you’re spending too much time trying to get your foot in the door and haven't made any serious stride towards implementing the feature after trying the steps above, it might be worth refactoring the code around your feature. Don't give up too easily, but also keep in mind what your deadlines are and what your project manager expects from you.
That said, there are drawbacks to going about it that way:
- Rewriting code can introduce bugs, though this can be somewhat circumvented with good unit testing.
- Rewriting code can remove hidden functionality, though this can also be circumvented with good unit tests.
- If you're pressed for time, writing code outside of your feature in addition to your feature might actually be more time-consuming than just building around it.
All in all, use your best judgement. There are pros and cons for either choice, and it's all dependent on your individual circumstances and project budget.
Now that we've covered the technical aspects of dealing with legacy code, let's talk about how to deal with it using our soft skills. After all, developers are people, not just coding robots, and dealing with challenging problems on projects that require creativity and authorship can be emotionally taxing, not only for you, but for your coworkers as well.
This is something that I will sheepishly admit I need to practice more. When I was first assigned the filter modal project, I was fairly vocal about how janky and unappealing the code was to deal with while the original author of the code was sitting 15 feet away from me. I intended my comments to be a joke, but in hindsight I recognize that I was being arrogant and hurtful, and that I should have been more empathetic.
There are a lot of factors that can lead to legacy code looking "off" which you should take into account before you start to criticize the author or assume the worst about them (This is loosely tied to the fundamental attribution error!).
Time constraints and technology constraints can cause a person to write code that works but doesn't necessarily have the best convention. If you picture yourself in a situation with not enough time, outdated tools, and a todo list a mile long, you probably wouldn't write the best code either!
In older Olio Apps projects, the convention for code is using single quotes to declare strings, and two spaces was equal to one tab. We had multiple small React components nested inside of a single file. In our current convention, we use double quotes and four spaces, and each React component, no matter how small, lives in its own
.tsx file in the
component directory. And in several years, I'm sure that will change too.
This ties back into the previous point: your code will eventually be legacy. As you move up the ladder of seniority, new developers will be hired on and will have to maintain your old code. You might write clean, flawless, DRY code, but once conventions change or trends change, those new developers might view your code the same way you view the legacy code of others.
It’s not easy to work outside of your habitual conventions; there's a reason for the huge trove of memes and jokes about dealing with legacy code. If you've ever learned a language outside of your native tongue, you know how it feels to forget a word or term in your second language, but remember it in your native language and not be able to translate across the gap. The same goes for changing between modern and legacy conventions. Sometimes it just takes a minute to regain your bearings.
In being able to successfully navigate legacy code, you're showing your ability to be adaptable, which is an important skill that benefits you at your current job and all of your future jobs, whether those jobs are in the technology field or not. Legacy code is the perfect playground to practice this skill.
Now that you've had the experience of working in a legacy codebase, you should come away from it with a better sense of what you like and don't like in terms of tools and conventions. These are things you can carry on with into future projects, and make you better at reviewing the code of others, offering constructive criticism, and giving mentorship.
Whether you've had the experience of great documentation and code comments or no documentation or code comments, you can see how both documentation and comments are powerful tools to help future developers navigate the project. You share a common goal of wanting a smooth, functional, and DRY app; maintaining the documentation and leaving behind informative code comments is a good way of bridging that gap.
I've mentioned this a few times already, but it's important to reiterate that your code will be legacy too, no matter how DRY and pristine your code and logic are.
The most important takeaway is to be flexible, humble, and that you can definitely learn new tricks from old code.