Back to books
Cover of Working Effectively with Legacy Code by Michael Feathers

Working Effectively with Legacy Code

by Michael Feathers · Published 2004

The single most useful book for the 90% of software work that isn't greenfield — 'legacy code' is redefined as 'code without tests,' and the seam-finding techniques are still the standard.

What works

  • Reframes 'legacy code' precisely (code without tests) in a way that makes the problem tractable
  • The seam and dependency-breaking techniques are directly, immediately applicable to real codebases

What doesn't

  • Code examples are Java/C++-era and slightly dated in syntax, though the techniques transfer fine
  • Not a book to read cover-to-cover for pleasure — it's a reference to dip into per problem

Summary

Michael Feathers opens with a definition that does most of the book's work: legacy code is code without tests. Not old code, not code in an unfashionable language, not code written by someone who left. Code you cannot change confidently because nothing will tell you when you break it.

That reframing turns an emotional problem into a technical one. "This codebase is horrible" has no next action; "this code has no tests, and I need to add some before changing it" does.

The obstacle is circular, and Feathers names it directly: to change code safely you need tests, but to add tests you usually have to change the code, because the code was not written to be testable. Dependencies are hard-coded, constructors do real work, and the class you want to test drags half the system in with it.

The book is a catalogue of techniques for breaking that cycle — finding places where behaviour can be altered without editing the code in place, isolating the piece you care about, getting a test around it, and only then making the change you were originally asked to make.

Key ideas

1. Seams and enabling points

The book's central concept. A seam is a place where you can change program behaviour without editing the source at that location, and every seam has an enabling point where the decision is made.

An object seam is the common case: if a class calls a collaborator through an interface, you can substitute a different implementation at construction. The enabling point is wherever that object is supplied.

Feathers also covers link seams (swapping libraries at link or load time) and preprocessor seams in C and C++. The value of the vocabulary is that it turns "this is untestable" into a search: where is the nearest seam, and what is its enabling point?

2. Sprout and wrap instead of editing in place

For the common situation where you must add behaviour to a method you cannot safely refactor, Feathers offers four techniques that avoid touching the existing logic.

Sprout Method puts the new behaviour in a new, tested method and calls it from the old one — the untested code gains a single line, and the new code is fully covered. Sprout Class does the same at class scale when the existing class is too entangled.

Wrap Method renames the original and creates a new method with the old name that calls both it and the new behaviour. Wrap Class applies the decorator pattern for the same purpose.

The shared principle is that new code should be testable even when the surrounding code is not, and that this is achievable today rather than after a cleanup that will never be scheduled.

3. Characterization tests describe, they don't specify

One of the most useful reframings for anyone facing undocumented code. A characterization test does not assert what the code should do; it asserts what it currently does.

The method is mechanical: write a test asserting something you know is wrong, run it, read the actual value from the failure, and change the assertion to match. Repeat until you have pinned the behaviour.

This dissolves the paralysis of not knowing the intended behaviour. You do not need to know — you need a record of current behaviour so that any change you make announces itself. Bugs get preserved too, deliberately, because something may depend on them.

4. Dependency-breaking is the core skill

The largest section of the book is a catalogue of roughly two dozen dependency-breaking techniques, each with a name, a procedure and worked examples.

Extract Interface, Parameterize Constructor, Extract and Override Call, Subclass and Override Method, Introduce Instance Delegator — most are small and mechanical, and their value is precisely that they are safe to perform without tests, because they change structure rather than behaviour.

Feathers is honest that some are ugly. Subclassing purely to override a method for testing is not elegant design; it is a step that lets you get a test in place, after which better design becomes possible.

5. Work in the direction of the change

A theme running through the book is that you should not attempt to fix a legacy codebase wholesale. Nobody will authorize it and it will not finish.

Instead, improve the code you are already touching. Feathers's approach is to identify change points, find the nearest seams, break the minimum dependencies needed, add tests, then make the change.

The cumulative effect is that the parts of a system under active development become well tested while dormant parts stay as they are — which is the correct allocation, since untested code that nobody modifies is a much smaller risk than untested code that changes weekly.

Who it's for

  • Anyone maintaining a codebase they didn't write — which is most working developers most of the time.
  • Developers who know TDD but inherit untested systems — this is the missing bridge.
  • Anyone tempted by a rewrite — the incremental alternative is laid out concretely.
  • Team leads planning refactoring — the change-point approach is a defensible strategy to propose.
This isn't a book to read cover to cover for pleasure — it's a reference to dip into per problem. Read the opening chapters on seams and characterization tests properly, then treat the dependency-breaking catalogue as something you consult when a specific class is fighting you.
The code examples are Java and C++ from 2004 and the syntax looks dated, particularly around dependency injection, which modern frameworks handle differently. The techniques themselves transfer fine to any object-oriented language, but you will be doing the translation yourself — expect to read past the surface of the examples to the structural move underneath.

FAQ

Do I need to know Java or C++?

No. The examples use them, but the techniques are structural and apply to any object-oriented language. Reading the code is straightforward even if you don't work in it.

Is it still relevant after twenty years?

Yes — arguably more so. Most software work is modifying existing systems, and the fundamental problem of testing code that wasn't designed for testing hasn't changed.

What if my code is genuinely beyond saving?

Feathers argues that's rarer than it feels, and that rewrites usually fail for the same reasons the original degraded. The incremental approach is slower but far likelier to finish.

Where should I start?

The seams chapter and characterization tests. Those two concepts change how you look at untestable code, and the rest of the book is largely applications of them.

How does it relate to Refactoring?

Fowler assumes you have tests; Feathers tells you how to get tests when you don't. Read Feathers first if your codebase is untested — it's the prerequisite.

Was this useful?

Counts appear once there are 5 votes.

More in this genre