A sociotechnical perspective on what makes code legacy, and what actually keeps it alive.
In the organization where I work, there is a mainframe COBOL system that has been running at the core of operations for decades. By every conventional definition, it should be legacy: old language, old architecture, a technology that most developers under forty have never touched.
It is not legacy. The team responsible for it is among the most mature and capable in the organization. They understand every decision embedded in that system: the constraints that shaped it, the business rules it encodes, the edge cases it handles. They can explain why a specific field is structured a certain way, what regulatory requirement drove a particular branching condition, and exactly what would happen downstream if you changed it. There is no question about the system they cannot answer.
Contrast that with a microservices architecture I reviewed at another organization. It had been built two years prior on a modern stack, with 85% test coverage and a full CI/CD pipeline. The team that built it had since been reorganized. No one could explain why the service boundaries had been drawn that way. Modifying anything felt like defusing a bomb. That system was legacy from the inside out, despite being less than two years old.
Michael Feathers defined legacy code as code without tests. The definition is pragmatic and useful, since tests reduce the uncertainty of change, but it is incomplete.
Tests are not the cause of human engagement with code. They are a partial substitute for its erosion.
When a team genuinely understands a system (its decisions, its constraints, its intended behaviors), tests are almost redundant as a knowledge preservation mechanism. When that understanding has eroded, tests become the last line of defense against the unknown. They matter enormously in that role. But their presence does not mean the understanding is there.
Legacy is not a property of the code. It is the state of the relationship between the code and the people responsible for it.
A system becomes legacy at the moment when no one can confidently answer: why does it work this way? Not how, since any developer can read code, but why: the reasoning, the trade-offs, the domain constraints that shaped the implementation. When that knowledge is gone, whether through team turnover, disengagement, or the gradual accumulation of changes nobody fully understood, the system has crossed the threshold.
This distinction matters because it changes what the problem actually is. If legacy is a technical property, the solution is technical: rewrite, refactor, add tests, modernize the stack. If legacy is a relational property (a broken link between a system and its human maintainers), then the solution requires restoring that link. Sometimes that means tests. More often it means something harder: rebuilding the shared understanding of what the system is supposed to do, and why.
Tests, documentation, and architecture diagrams are mechanisms for transmitting intent across time and team boundaries. They matter, but they are proxies for something more fundamental: the capacity of a human being to understand a system well enough to change it with confidence.
As long as that capacity exists, the software is alive, regardless of its age, its language, or its test coverage.
The moment it disappears, the system becomes legacy. And no amount of refactoring will fix that without first addressing the human dimension.
This is the foundation on which the rest of this series is built. The next article takes up the question that follows: what happens to that human capacity when AI generates the code?
Gabriel Mailhot
Software Architect and Software Engineering Quality Advisor
Series: What AI Cannot Replace in Software Development