I read through this book relatively recently and agree with the praise here for the core idea that legacy code is code that is untested. The first few chapters are full of pretty sharp insights that you will nod along to if you've spent a decent amount of time in any large codebase.
However, most of the content in the last half of the book consists of naming and describing what seemed like obvious strategies for refactoring and rewriting code. I would squint at the introduction to a new term, try to parse its definition, look at the code example, and when it clicked I would think "well that just seems like what you would naturally choose to do in that situation, no?" Then the rest of the chapter describing this pattern became redundant.
It didn't occur to me that trying to put the terms themselves to memory would be particularly useful, and so it became a slog to get through all of the content that I'm not sure was worth it. Curious if that was the experience of anyone else.
I would argue that legacy code has more chance of going along with test code than new code. Often code is written and tests grow during its maintenance.
Another thought... if I'm tasked with maintaining or adding features to existing code, should I feel responsible for writing tests for the existing codebase?
I recently talk therapied some legacy code through Claude Code back to life, but along the way put some frameworks in place for the codebase itself, and overall with how I think about approaching existing implementations, which helped a ton to shed a path forward for modernization.
It ultimately reminded me there's always a ton more to learn and think about, and it's sometimes quickest to learn from others experiences when considering paths forward, so the book is a helpful share, thanks.
All code is eventually legacy code in one way or another, whether it's the code directly, or the infrastructure level. Today's code might be the best one writes today.. looking back in 5-10 years.. one might wonder if best practice was best practice.
What sort of LLM/Copilot advice file could you write to encapsulate all the good advice here?
Would "approach this problem using the ideas from Michael Feather's book Working Effectively With Legacy Code" work?
Kind of depressing to read because it's so accurate, yet so likely to be ignored.
I remember when Martin Fowler first published the book "Refactoring" - I was so relieved at the time because somebody with some clout that executives might actually listen to had not only identified what was wrong with software but identified a way to fix it and even gave it a name! Boy was I wrong - now you have to be careful how and when you use the term "refactor" because the people who ought to be supportive hear "wasting time".
This is on the engineers. Asking for time to refactor is like a painter asking for time to prime. It’s part of the job so just do it. When brought out in discussion, it sounds as if it is optional and its value dubious. That’s where the feeling of “wasting time” comes from.
Additionally, a lot of refactoring is about predicting the future rather than adapting the code to present expectations while keeping things easy to change. The only metric that your future self cares about is time to feedback. Refactor to adapt to present expectations while keeping the code easy to change in the future, and don’t ask for permission to do this- it is your job.
All the blockquotes are coming in as black on black for me in Safari, Firefox, and Chrome. This appears to be due to a css file called "https://understandlegacycode.com/_astro/ai-support.D3anziw5....". Anyone wanna lay odds on whether that filename is a big tell that much of the text is also written by three autocompletes in a trenchcoat?
I understand and I agree with the author points, specially looking to distance yourself from the dependencies these systems are usually entangled with, however:
>But the code examples are in Java and C++ and I do python/JavaScript/ruby/
The problem with real legacy code is that sometimes it's not even in those languages. It's VB.NET, COBOL, AS400, BASIC, FORTRAN...and these may not have a chance to "wrap around your class", or "think about all the ORM code that you use". None! I use none of that!. And I can't even call any tests because I can't extend a non existant class, there's no objects in here!.
The author also says:
>You need feedback. Automated feedback is the best. Thus, this is the first thing you need to do: write the tests.
I don't need automated feedback. I need to untangle this deep business layer that everything is wrapped around in a 40 years old codebase, with practically no documentation and having to modify one of the core systems of the company. Sometimes I can't even trust the feedback the program outputs. In this kind of scenarios where the code is so messy and limited by technical factors, the best approach I have found is to debug. And that's it. Follow the trail, find the "seam", and then start your kingdom in the little space you can. Because if you tell your boss that you are implementing Unit tests in a 40 years old codebase, the first question he is gonna hit you with is "Why?", and the article doesn't give any compelling argument to answer this.
Big caveat that I’ve not read the book but I get that the premise is “legacy code is code that is untested” and I’ve always struggled with that definition. I think a more accepted definition would be that it is not well understood, uses old tooling/languages/runtimes and is usually high stakes code (driving some business that was successful enough to last longer than the staff that coded it up). I’m sure added tests help deal with legacy code under that definition but not sure it’s the panacea, I’ve certainly seen code without tests lots very high coverage that people area afraid to work on due to them being unfamiliar with the domain and codebase.
The code base I work on has multiple layers of wrapper code from people following this sort of advice. Sure, it's safe, but once the code is two layers of wrappers deep, you'll see the downsides.
I've replaced a few large legacy systems over the years and I disagree with most of the points mentioned in the link. The only way guaranteed to work is to run the old system concurrently with the new system with live inputs and compare results for a trial period and fixing discrepancies as they arise. Only when there is 100% behavioral/data fidelity with the old system can a switchover to the new system occur.
You're assuming the "legacy" system is being replaced by a new one, which isn't the scenario being described in the link or the book. They cover approaches for continuing to safely evolve the legacy (i.e. untested) system.
I once had to replace a PostgreSQL-interfacing layer of a large+complex+important system, and most of the original tests had been lost.
It's the kind of change that could be an existential risk for a company that needs the system to be available and correct.
So I built a comprehensive test suite for the existing PostgreSQL-interfacing layer, tested it, made drop-in compatible API using the new way to talk with PostgreSQL, tested that, and... the entire huge system simply worked with the replacement, without a lot of headache/defects/dataloss/ulcers.
> Before you change code, you should have tests in place. But to put tests in place, you have to change code.
Keep reading. You don't always have to do it in the heroic way the article described in this section. Modularity and backward-compatible interfaces, FTW, when you can. The "Sprout" and "Wrap" techniques that the article describes later.
Also, one complementary general technique that the article didn't get into is that sometimes you can run two implementations in parallel, and check them against each other. There are various ways you can do this, involving which really implementation really takes effect, and what you do if they don't agree. (Related: voting configurations in critical systems.)
> Characterization tests
The test suite not only let me validate the implementation, but the exercise of developing the test suite also helped me understand exactly what I should be implementing in the first place.
Sometimes I had to look at the implementation, to tentatively figure out the semantics, then testing validated that also.
> Use scratch refactoring to get familiar with the code
In reasonable cultures and with reasonable management, or if you don't tell anyone. Otherwise, it's the same kind of risk as the old "throwaway prototype" problem: someone with authority says "looks good; I told the customer/CEO we just need to make these few more changes, and ship it". Or, in popular Agile/Scrum processes, "I thought you already completed the refactor last sprint; why are you doing it again".
No. Run away from any sort of refactor or rewrite. Don't even think about it. Do not proceed. If you're thinking that you can do better, you will be humbled in the worst way. Ignore my warning at your peril. I'll never again for the rest of my life make the horrible mistake of rewriting any system. The article's tactics are all good advice – but it's like tactics for surviving on the front line. You win by never going there in the first place.
However, the big problem with legacy code is always political. And the only book I've seen ever try to address that is "Kill It with Fire".
It's one of the few books that talks about the need to keep your team motivated and plugged into the political system given that they are working on the thing that's super important but also almost invisible. You need to be constantly selling what is going on in front of the important eyeballs.
Unfortunately, a lot of the anecdotes have an undercurrent of "Until the system has actively exploded into a million pieces such that somebody with real power has their ass on the line, don't bother."
It's easy to say 'write tests', but it's difficult to do so if you don't know what the actual requirements are.
People often think they are doing it 'properly' now by starting simple, but as they learn more and add functionality, they end up with the same complex mess they wanted to avoid.
Love this resource, really looking forward to perusing the full text.
And to echo what others are saying I have spent the last month experiencing for the first time how not simple throwing a meaningful test suite together is for a ginormous legacy codebase. I was glad the author briefly seemed to acknowledge that but still... the pain...
They said the playwright ui was supposed to make it easy... Just plug it into an LLM they said... It should just take a few days right?
Legacy code is code you didn't write. It used to be that code you didn't write was written by past team members. These days, we have legacy-code-generators that write code you didn't write at a much higher rate then your company can fire team members.
The only issue I have with writing tests before refactoring is that sometimes it can take a really long time to write those tests and cover all the cases, not to mention needing to sometimes refactor to make the code even testable.
I've made huge successful refactors using nothing but carefulness and manual testing, maybe just a very little bit of unit testing :)
I like to make the distinction that the absence of automated tests is not the absence of tests altogether, and that the time it takes to get an automated test set up might be longer than doing the test manually. This is especially important to be aware of if the tests will be temporary because the code is about to be changed.
Of course the goal is to reach an end state with automated tests, but insisting on 100% automated tests up-front can actually prevent the refactoring from even starting.
I feel like there is a need for a new paradigm on navigating code; and reference documentation for that matter.
I like how https://gwern.net and https://en.wikipedia.org allow you to hover over terms and show you a quick glance of a term you might not know, and then you can fan out to create an understanding for the basis needed to understand the material in front of you. I feel like some sort of windowing system where I can pin and move references around and then kinda have the llm explain or reference things might be of use to me, as in help me navigate the unknown. Is there a "study companion" application of the sort?
I find that this is easier to do with a language like Java than say Python, I need as much information as I can in order to understand the decisions that led to the current implementation.
Sometimes when I want to change some piece of legacy code, I need to know the rationale behind the code written here, although keeping the interfaces the same and changing the implementation allows me to go quite far.
This book talks about unit tests, but for UI they are mostly useless. I found that for UI the most essential refactoring tools are storybook and visual diff tests.
This book is extremely overrated because the whole thing can be summed up in two bullet points:
* write your new code somewhere outside the main solution and call it from the old solution
* automated tests are pretty cool and you should write these whenever possible
I found the book to be close to useless when I was working with actual legacy code. How do I write an unit test for an application that has an embedded Visual Basic script support with dozens of scripts that are thousands of lines long? The book does not answer this question at all because the author assumed I have the luxury of working with Java or an another language that supports the object-oriented paradigm and can be endlessly extended with no limitations whatsoever.
I think the riskiest migrations are the ones where migration is done in parallel. Internal org power struggles get leaked, whole teams are lobbying against the effort. And on top of all of that rediscovering many things all over again. That in response to cases where a company might decide that the fix too all legacy -> new system is by throwing more money at the problem.
31 comments
[ 3.7 ms ] story [ 52.2 ms ] threadHowever, most of the content in the last half of the book consists of naming and describing what seemed like obvious strategies for refactoring and rewriting code. I would squint at the introduction to a new term, try to parse its definition, look at the code example, and when it clicked I would think "well that just seems like what you would naturally choose to do in that situation, no?" Then the rest of the chapter describing this pattern became redundant.
It didn't occur to me that trying to put the terms themselves to memory would be particularly useful, and so it became a slog to get through all of the content that I'm not sure was worth it. Curious if that was the experience of anyone else.
Another thought... if I'm tasked with maintaining or adding features to existing code, should I feel responsible for writing tests for the existing codebase?
I recently talk therapied some legacy code through Claude Code back to life, but along the way put some frameworks in place for the codebase itself, and overall with how I think about approaching existing implementations, which helped a ton to shed a path forward for modernization.
It ultimately reminded me there's always a ton more to learn and think about, and it's sometimes quickest to learn from others experiences when considering paths forward, so the book is a helpful share, thanks.
All code is eventually legacy code in one way or another, whether it's the code directly, or the infrastructure level. Today's code might be the best one writes today.. looking back in 5-10 years.. one might wonder if best practice was best practice.
I remember when Martin Fowler first published the book "Refactoring" - I was so relieved at the time because somebody with some clout that executives might actually listen to had not only identified what was wrong with software but identified a way to fix it and even gave it a name! Boy was I wrong - now you have to be careful how and when you use the term "refactor" because the people who ought to be supportive hear "wasting time".
Additionally, a lot of refactoring is about predicting the future rather than adapting the code to present expectations while keeping things easy to change. The only metric that your future self cares about is time to feedback. Refactor to adapt to present expectations while keeping the code easy to change in the future, and don’t ask for permission to do this- it is your job.
>But the code examples are in Java and C++ and I do python/JavaScript/ruby/
The problem with real legacy code is that sometimes it's not even in those languages. It's VB.NET, COBOL, AS400, BASIC, FORTRAN...and these may not have a chance to "wrap around your class", or "think about all the ORM code that you use". None! I use none of that!. And I can't even call any tests because I can't extend a non existant class, there's no objects in here!.
The author also says:
>You need feedback. Automated feedback is the best. Thus, this is the first thing you need to do: write the tests.
I don't need automated feedback. I need to untangle this deep business layer that everything is wrapped around in a 40 years old codebase, with practically no documentation and having to modify one of the core systems of the company. Sometimes I can't even trust the feedback the program outputs. In this kind of scenarios where the code is so messy and limited by technical factors, the best approach I have found is to debug. And that's it. Follow the trail, find the "seam", and then start your kingdom in the little space you can. Because if you tell your boss that you are implementing Unit tests in a 40 years old codebase, the first question he is gonna hit you with is "Why?", and the article doesn't give any compelling argument to answer this.
- Anything with millennial-looking cartoon characters in cartoons is bad.
- Anything with furry characters is good.
I'm not aware of the formal Design Pattern name, but a google search yielded this blog post on the subject: https://medium.com/@sahayneeta72/parallel-run-strategy-7ff64...
Coincidentally this is the same strategy github employed when verifying libgit2: https://github.blog/engineering/engineering-principles/move-...
This can be a superpower.
I once had to replace a PostgreSQL-interfacing layer of a large+complex+important system, and most of the original tests had been lost.
It's the kind of change that could be an existential risk for a company that needs the system to be available and correct.
So I built a comprehensive test suite for the existing PostgreSQL-interfacing layer, tested it, made drop-in compatible API using the new way to talk with PostgreSQL, tested that, and... the entire huge system simply worked with the replacement, without a lot of headache/defects/dataloss/ulcers.
> Before you change code, you should have tests in place. But to put tests in place, you have to change code.
Keep reading. You don't always have to do it in the heroic way the article described in this section. Modularity and backward-compatible interfaces, FTW, when you can. The "Sprout" and "Wrap" techniques that the article describes later.
Also, one complementary general technique that the article didn't get into is that sometimes you can run two implementations in parallel, and check them against each other. There are various ways you can do this, involving which really implementation really takes effect, and what you do if they don't agree. (Related: voting configurations in critical systems.)
> Characterization tests
The test suite not only let me validate the implementation, but the exercise of developing the test suite also helped me understand exactly what I should be implementing in the first place.
Sometimes I had to look at the implementation, to tentatively figure out the semantics, then testing validated that also.
> Use scratch refactoring to get familiar with the code
In reasonable cultures and with reasonable management, or if you don't tell anyone. Otherwise, it's the same kind of risk as the old "throwaway prototype" problem: someone with authority says "looks good; I told the customer/CEO we just need to make these few more changes, and ship it". Or, in popular Agile/Scrum processes, "I thought you already completed the refactor last sprint; why are you doing it again".
However, the big problem with legacy code is always political. And the only book I've seen ever try to address that is "Kill It with Fire".
It's one of the few books that talks about the need to keep your team motivated and plugged into the political system given that they are working on the thing that's super important but also almost invisible. You need to be constantly selling what is going on in front of the important eyeballs.
Unfortunately, a lot of the anecdotes have an undercurrent of "Until the system has actively exploded into a million pieces such that somebody with real power has their ass on the line, don't bother."
People often think they are doing it 'properly' now by starting simple, but as they learn more and add functionality, they end up with the same complex mess they wanted to avoid.
And to echo what others are saying I have spent the last month experiencing for the first time how not simple throwing a meaningful test suite together is for a ginormous legacy codebase. I was glad the author briefly seemed to acknowledge that but still... the pain...
They said the playwright ui was supposed to make it easy... Just plug it into an LLM they said... It should just take a few days right?
> You have to deal with Legacy Code every day.
Legacy code is code you didn't write. It used to be that code you didn't write was written by past team members. These days, we have legacy-code-generators that write code you didn't write at a much higher rate then your company can fire team members.
I've made huge successful refactors using nothing but carefulness and manual testing, maybe just a very little bit of unit testing :)
Of course the goal is to reach an end state with automated tests, but insisting on 100% automated tests up-front can actually prevent the refactoring from even starting.
I like how https://gwern.net and https://en.wikipedia.org allow you to hover over terms and show you a quick glance of a term you might not know, and then you can fan out to create an understanding for the basis needed to understand the material in front of you. I feel like some sort of windowing system where I can pin and move references around and then kinda have the llm explain or reference things might be of use to me, as in help me navigate the unknown. Is there a "study companion" application of the sort?
I find that this is easier to do with a language like Java than say Python, I need as much information as I can in order to understand the decisions that led to the current implementation.
Sometimes when I want to change some piece of legacy code, I need to know the rationale behind the code written here, although keeping the interfaces the same and changing the implementation allows me to go quite far.
* write your new code somewhere outside the main solution and call it from the old solution
* automated tests are pretty cool and you should write these whenever possible
I found the book to be close to useless when I was working with actual legacy code. How do I write an unit test for an application that has an embedded Visual Basic script support with dozens of scripts that are thousands of lines long? The book does not answer this question at all because the author assumed I have the luxury of working with Java or an another language that supports the object-oriented paradigm and can be endlessly extended with no limitations whatsoever.