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.

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.

The hardest part of working with legacy code isn't understanding what it does — it's changing it safely without a test suite to catch mistakes. The seam-and-characterization-test approach exists specifically to build that safety net incrementally, without requiring the code to be perfect, or rewritten, first.

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.