Legacy Code
Code that is risky to change because its behaviour is poorly understood or weakly protected — and the characterization-test, seam, small-step loop that makes it safe.
Legacy code is code that is risky to change because its behaviour and assumptions are poorly understood or weakly protected. Age is a correlate, not the definition.
Tests that record what the code does today — including what it does wrongly — so that a later change has a baseline to be measured against. They assert behaviour, not correctness.
A seam is a place where you can change behaviour, or substitute a dependency, without editing the code at that place. Finding one is what makes untestable code testable.
Understand, characterize, seam, small refactor, change behaviour, verify — in that order, because each step is what makes the next one safe rather than brave.
Put a routing layer in front of the old system, move one behaviour at a time behind it, expand until nothing is routed to the old system, then retire it. The old and new run together for a long time.
Rewrites fail for four specific reasons — hidden requirements, the delivery gap, migration complexity and feature-freeze pressure. They are sometimes still the right call, and the conditions are nameable.
Old and new coexist; you migrate one slice, verify it against reality, and repeat. The design work is choosing the slice and defining what "verified" means.