What is Legacy code?

Engineering culture

Legacy code is code that is difficult to change safely. In engineering culture, that usually means the software carries history, hidden assumptions, and missing feedback loops such as weak test coverage or scarce living knowledge. It does not simply mean "old code". A twenty year old system can be dependable and well understood. New code can become legacy in weeks if nobody can change it with confidence.

What this means

People outside software often hear "legacy code" and imagine a museum piece, dusty but harmless. Engineers usually mean something more practical. They mean code where each change feels risky because the behaviour is not easy to verify. The age of the code may contribute, but age alone is not the point.

That is why one influential definition says legacy code is code without automated tests. The phrasing is deliberately sharp. It shifts attention away from birthdays and toward confidence. If a team cannot check what the code currently does, even a small change can feel like surgery in the dark.

Why it matters

This term matters because it changes the conversation from embarrassment to risk. A business critical old system is not necessarily a failure. In many companies it is the thing that still invoices customers, settles payments, schedules staff, or keeps a supply chain moving. Calling it legacy is often less a sneer than an admission that the organisation cannot change it casually.

It also matters because "legacy" can become a dangerous simplification. Teams sometimes use it to mean old, ugly, written by somebody else, unfashionable, or annoying. Those are feelings, not diagnoses. If you understand what engineers usually mean by legacy code, you are in a much better position to judge timelines, staffing, and modernisation plans realistically.

How it works

What people usually mean by it

The famous shorthand is "code without tests", and it remains useful because it gets straight to the issue of safe change. Tests are automated checks that tell you whether behaviour still matches expectation after a change. Without them, every edit relies more heavily on memory, manual checking, and nerve.

But the lived meaning is a bit wider. Code feels legacy when knowledge has thinned out. The original authors have left. The integrations are poorly mapped. The feedback loops are slow. Build times are long. Important behaviour only reveals itself in production. Logs are sparse. Every change seems to touch several fragile areas at once. In that environment, even tidy looking code can be legacy.

How code becomes legacy

Ironically, success is one route. Software that survives long enough to matter accumulates regulations, customer quirks, partner APIs, bug fixes, workarounds, and postponed clean-ups. Over time the code starts to reflect not just a design, but years of negotiated reality. That is why old systems can be both ugly and valuable. They have learned things.

Another route is haste. Teams under delivery pressure often ship first and postpone testability, observability, or documentation. If those habits continue, the code can become legacy almost immediately. A brand new service with no tests, tangled dependencies, and one engineer who understands it is already legacy code in embryo.

Some legacy systems are also hard to change because they have poor seams. A seam is a place where you can alter behaviour without tearing straight into the heart of the system. If everything is tightly coupled to databases, files, external services, or global state, a small change can force a large blast radius. That is why experienced engineers spend so much time trying to introduce seams before they attempt major edits.

How teams work with legacy code in practice

Here lies one of software's more frustrating loops. To change risky code safely, you want tests. But to get tests around the code, you often need to change the code first. Michael Feathers called this the legacy code dilemma, and every engineer who has inherited a brittle system recognises it at once.

The way through is usually incremental. You read the existing code slowly. You make small, conservative changes that introduce a seam, extract a side effect, or isolate a dependency. You build just enough automated checking to understand current behaviour. Only then do you start making the structural improvements people imagine when they casually say "refactor it".

This is one reason the cultural mythology around legacy code is often wrong. People speak as if the old system is a swamp and the new system will be a palace. In reality, the old system may contain thousands of hard won bug fixes and domain rules that nobody has fully written down. Joel Spolsky once made the brutal point that old code does not rust, it often gets better as bugs are discovered and fixed. That does not make it pleasant. It does make it dangerous to dismiss.

Reading legacy code also requires a slightly different mindset from reading prose. It is slower and denser. You cannot just skim it and decide you have understood it. Teams get into trouble when they mistake that discomfort for proof that the code is worthless. Sometimes the code is bad. Sometimes it is merely unfamiliar. The distinction matters a great deal.

Examples

A payroll system written years ago still handles the nastiest edge cases correctly because it has already met real tax rules, real exceptions, and real historical data. Nobody loves touching it, but replacing it without a patient map of those edge cases would be reckless.

A customer support tool mixes database access, page rendering, and business rules in the same files. Adding one field means touching multiple screens and several query paths. The problem is not mainly that the code is old. It is that safe change has become expensive.

A service calls a live external API from the middle of core business logic, so any test hits a real dependency and behaves unpredictably. The first useful step is not a grand redesign. It is introducing a seam that lets the team replace the live call with a controlled test double.

Common misunderstandings

Legacy code does not just mean old code. Some old systems are stable, well tested, and easier to change than last month's startup experiment.

It also does not simply mean "code I did not write". Inherited code can be excellent. Fresh code written by your own team can be legacy the moment nobody can explain or verify it safely.

Another myth is that legacy code must be rewritten to improve it. Quite often the safer path is gradual hardening, adding tests, introducing seams, improving observability, and reshaping one area at a time.

And tests are not a magic wand. They improve confidence, but they do not automatically untangle poor boundaries, remove duplication, or erase confusing domain rules. They make deliberate improvement possible, which is different.

Risks and boundaries

"Legacy" can become an excuse, either for avoiding responsibility or for insulting previous engineers. Old systems usually reflect the constraints of their time, deadlines, infrastructure limits, immature libraries, customer pressure, and business choices. Treating them as evidence of personal failure is childish and leads to bad modernisation decisions.

There is another boundary too. Teams should not romanticise legacy systems merely because they still run. If code is slow to change, thinly understood, under-tested, and operationally risky, politeness does not require denial. Respecting the past should make the present diagnosis calmer, not softer.

What to do next

If you recognise this pattern in your team, stop planning around heroics. Legacy work improves when engineers have protected time to add tests, create seams, improve logs, write small notes about weird behaviours, and reduce feedback time. Those activities look indirect only if you do not understand what confidence costs.

Make domain knowledge portable. Record the nasty edge cases. Keep examples of real behaviour. Pair newer engineers with people who know the rough corners. The most dangerous legacy system is often the one that lives mainly in one person's head.

Finally, be very careful with the phrase "just rewrite it". If what you really have is a confidence problem, then the first budget should usually go toward understanding and stabilising the existing system rather than declaring war on it.

FAQs

Is all old code legacy code?

No. Age can contribute, but legacy is mainly about how hard it is to change safely and how much confidence a team has in its understanding.

Can brand new code be legacy?

Yes. If it ships with poor boundaries, little test coverage, and thin shared knowledge, it can become legacy almost immediately.

Why do people say legacy code is code without tests?

Because tests are one of the clearest ways to gain confidence in change. Without them, even simple edits can be risky.

Is legacy code always bad code?

No. Some legacy systems are valuable, reliable, and full of real world knowledge. They may still be hard to change, which is a different issue.

What is a seam in legacy work?

It is a place where you can alter behaviour without tearing into the whole system, which helps teams isolate dependencies and add tests safely.

Should AI make legacy code easy to modernise?

AI can help explain and navigate, but it does not remove hidden behaviour, missing tests, or operational risk. The hard parts remain real.

Sources