Michael Feathers, in Working Effectively with Legacy Code, offers a definition that deliberately has nothing to do with how old a codebase is: legacy code, in his framing, is simply code without tests. A codebase written last month with no test coverage qualifies just as much as a codebase written fifteen years ago — what makes code “legacy” in the sense that matters for how carefully you have to work with it is the absence of a safety net that tells you whether a change broke something, not the code's vintage. One gradual modernization strategy is the Strangler Fig pattern.
Why the age-independent definition matters
This reframing changes what the actual problem is. If legacy code were primarily about age, the fix would be rewriting old things. Because it's actually about test coverage, the fix is adding tests — which is a fundamentally different, more tractable problem than a full rewrite, and one that can be done incrementally, to exactly the parts of the code that are about to be changed, rather than requiring a large, separately justified project. The same trade-off mindset applies to staffing and workflow design; this guide explores that operational angle.
The core tension the book addresses directly: to safely add a test to existing code, you often need to first change the code slightly, to make it testable in isolation — but changing code without tests is exactly the risky activity you're trying to avoid. Feathers' answer to this apparent chicken-and-egg problem is the concept of a seam: a place in the code where behavior can be altered without directly editing the code in that exact spot, usually by substituting a dependency, which allows a test to be introduced safely before any larger change is made.
Characterization tests: a pragmatic starting point
For code with no tests and unclear, undocumented intended behavior, the book proposes characterization tests — tests that simply record what the code currently does, correct or not, as a baseline, rather than tests that assert what the code should do according to some specification that may not exist or may not be accurately remembered by anyone still on the team. This is a deliberately modest starting point: the goal isn't yet to verify correctness, it's to create a safety net that will catch any accidental change in behavior once real modifications begin, which is exactly the property refactoring, discussed elsewhere on this shelf, depends on to be verifiable at all.
- Legacy, by this definition, is about test coverage, not age — untested new code carries the same risk profile as untested old code.
- Look for seams — places where a dependency can be substituted — before attempting to add tests directly to tightly coupled, hard-to-isolate code.
- Characterization tests record current behavior as a baseline; they don't claim the current behavior is correct, only that any deviation from it should be a deliberate, visible decision.
- Add tests specifically to the code you're about to change, rather than attempting to comprehensively test an entire legacy system before touching any of it — the incremental approach is what makes this tractable at all.
Why rewrites are usually the wrong instinct
Confronted with code that's hard to work with, the common instinct is proposing a full rewrite, which is almost always riskier than it appears: a rewrite discards not just the code's structural problems but also all the accumulated, often undocumented edge-case handling the original code has accreted over its lifetime — handling for situations the rewrite's authors may not know exist until the new version ships and the same old bugs resurface, freshly reintroduced. The incremental, seam-based approach Feathers describes is less dramatic but reliably lower-risk, because it never discards working behavior wholesale, only the specific piece currently being changed.
Read alongside the Boy Scout Rule and disciplined refactoring, both discussed elsewhere on this shelf, this gives a complete, small-steps alternative to the rewrite instinct: add a test via a seam, refactor safely now that the test exists, repeat — slower than a rewrite in the short term, and considerably safer over the life of the system.