Working with legacy code

Legacy code is code you’re afraid to change — the fear is the defining property, not the age. Most code in the world is legacy code: it earns money, nobody fully understands it, and the tests either don’t exist or don’t inspire trust. The discipline is about converting fear into evidence.

The problem decomposes into three nested loops:

  • Understanding — what does this even do? Dependency graphs, behavioral analysis from VCS history (who touched what, when, why — the file-history habits in Git commit messages and piechowski.io’s read-before-code commands are this loop’s tools), CIA git tips for archaeology.
  • Safety netting — before changing, put tests around the behavior you’re about to touch. The classic techniques: Approval Testing (golden-master snapshots of current behavior — even wrong behavior — so refactoring can’t silently change it), characterization tests, seam-based test injection. Related: the regression-tests/auto-bisect item already captured from Git Legit.
  • Changing — small, verified steps. The Mikado Method (graph of prerequisites before you touch anything), strangling one call site at a time — see Fearless refactoring for the social mechanics that keep even righteous changes from dying in review limbo.

The interplay with agents: LLMs change the economics of all three loops — comprehension summaries of foreign code, generated characterization tests, mechanical migration of call sites — but the evidence bar (tests that pin actual behavior) stays exactly where it was. Agent tests kinship.

Related: Fearless refactoring (the social half of the same problem) · Performance engineering (measure before deleting) · Musk engineering process (delete-first applies to legacy too — the hardest legacy instinct to unlearn is preserving)

Resources

  • Understand Legacy Code (Nicolas Carlo; captured → clippings/understandlegacycode-com.md) — the reference site: technique pages (Approval Tests, Behavioral Analysis, Dependency Graphs, Changing Untested Code, Getting Into a Large Codebase, Code Feels Impossible to Maintain, Best Practice or Code Smell?), the First Aid Kit, and the book overviews — notably Working Effectively with Legacy Code (Feathers), the discipline’s founding text, plus Refactoring at Scale and the Mikado Method material.
  • Feathers, Michael — Working Effectively with Legacy Code (2004, Pearson) — the founding text: legacy code = code without tests; the Legacy Code Dilemma (to change safely you need tests, to test you need to change); seams as the unit of attack (places where behavior can be varied without editing the code — link seams, object seams, preprocessors); fast, reliable unit tests as the only trustworthy safety net; characterization tests that pin current behavior, bugs included; Sprout & Wrap when there’s no time to refactor (sprout a new method/class for new behavior; wrap the old behind an interface); scratch refactoring to learn a codebase you’re allowed to throw away; don’t depend on libraries’ implementation details. Overview captured → clippings/understandlegacycode-com-blog-key-points-of-working-effectively-with-legacy-code.md (Nicolas Carlo’s key-points summary of the book).
  • How to Improve a Legacy Codebase (Jacques Mattheij; captured → clippings/jacquesmattheij-com-improving-a-legacy-codebase.md; HN 653 pts / 293 comments → clippings/hn-14444914.md) — the field-manual companion to Feathers, ordered as an operating sequence: backup everything read-only first → establish a build that actually produces what runs in production → freeze the DB schema → write tests → instrument and log → change only one thing at a time → platform changes before architecture changes before low-level refactoring → fix bugs → only then execute on the roadmap. The two iron rules: never mix maintainability work with feature work, and never attempt a big-bang rewrite (“you’ll end up realizing that the guys that put the old system together weren’t maybe idiots after all”). The HN thread is the organizational counterweight: how to sell refactoring without a feature freeze (refactor-the-parts-around-X, elongate estimates; bugs/performance as the sellable framing), and the crdoconnor–DougWebb debate on whether engineers should have to make business cases at all.
  • The captured Git-family notes cover the archaeology loop; the redesign-fear pair (clippings/agileotter-…why-redesign-doesnt-happen-enough-html.md + clippings/blog-ploeh-dk-…redesign-without-fear.md) covers why changes stall socially.