Legacy Code Is a Risk Register, Not a Birth Certificate
Procurement scores install dates. Delivery leads need to know what breaks when someone edits a rounding rule — and who still owns the answer.
In the warehouse-management upgrade pack, someone filed the nightly inventory reconciliation job under legacy — fifteen years in production, therefore replace. The ERP analyst who owned the thread asked what would break if tax rounding changed before go-live. The replies that arrived over the next three days restated vintage, cited the vendor roadmap, and never named a test that would catch a regression. Install year was the only evidence anyone would sign.
Procurement sorts systems by install year. Auditors ask for end-of-support dates. Delivery leads need a different question.
What is the cost of the next change, and where does that cost actually live? Legacy code, when it matters on a production Tuesday, is code you cannot change with confidence — and the bill is rarely printed in the repository alone.
The label everyone uses — and the one that survives a change request
There is an emotionally neutral definition: legacy code is old code you keep because it still works. Practitioners use a sharper one. Michael Feathers, in Working Effectively with Legacy Code, defines legacy code as code without tests. Not "unfashionable." Not "written in a language HR struggles to hire for." Without a fast, repeatable way to verify that a change preserved behaviour, every edit is a small bet against production.
The age of the code has nothing to do with it. People are writing legacy code right now — on your programme, possibly in last week's merge — when they ship without tests and without a runbook the next shift can follow.
Calendar age is seductive because it is easy to measure. It is a dangerous sorting key. A three-year-old service with one author, opaque module boundaries, and zero automated checks can be legacy before the COBOL in the next rack is. A twenty-year-old batch job with characterisation tests, a maintained runbook, and two engineers who have rotated through support is often just old code that still earns its keep. The label should follow change risk, not vintage.
Technical debt is the first signal — and it was never just the repo
Ward Cunningham coined technical debt in 1992 while explaining why a financial product needed periodic consolidation, not perpetual patch layers — shipping first-time code is like going into debt. A little debt speeds delivery if you pay it back with a rewrite. Every minute spent on not-quite-right code counts as interest. Fail to consolidate and the programme becomes unmasterable — extreme specialisation of programmers, finally an inflexible product.
Martin Fowler's later framing is useful for practitioners: cruft in module structure is interest on future features. You can pay principal gradually in the modules that change every sprint. Stable-but-ugly corners can wait until someone has to touch them. What you cannot do is pretend the interest line item lives only in Jira.
Cunningham's original report is about team mastery, not a ticket queue. Debt accrues when the team's shared model of the system drifts from the code's actual shape — when consolidation keeps getting deferred because the quarter needed a feature.
Fowler links the metaphor to human costs: infighting over who broke what, skills that atrophy because nobody dares refactor, attrition among the people who still remember why the guard clause exists. Technical debt was never only a repository concern. It was always a delivery-culture concern with a git blame view.
Social debt compounds technical debt
When strained interactions between teams slow delivery, practitioners call the accumulated cost social debt — the organisational bill for a development community that is not quite right yet. Friction between people that shows up as delays and rework, not as a compiler error: that is the one-sentence version worth keeping. Tamburri and colleagues, studying a large integration programme, found social debt strongly correlated with technical debt. Uninformed socio-technical decisions can generate both at once, in a compounding way that a refactor sprint cannot trivially repay.
Their case is familiar. Two sites worked on one integration. Product management and architecture decisions concentrated at Site A. Site B executed requirements it could not easily challenge. The result was not malice — it was structure. Information filtered through formal channels. Duplicated logic appeared because neither side owned the boundary conversation. Code churn rose because coordination needs were misread as individual coding errors.
The coordination gap shows up in the diff before it appears on the programme plan.
Researchers call precursors to this pattern community smells: organisational silos, delayed information sharing, ownership that follows a reorg chart rather than the dependency graph. They behave like code smells — early warnings, not verdicts. A team that hoards runbook knowledge in one person's inbox is storing social debt next to the technical debt in the untested module that person alone understands.
Refactoring the module without fixing the smell repays the wrong loan. You get cleaner code and the same bus factor, which means the next urgent change lands on the same nervous specialist at the same overtime rate.
Conway's mirror — architecture frozen as org chart
Melvin Conway observed in 1968 that system design reflects the communication structure of the organisation that produced it. Modules and team boundaries tend to align — not as metaphor, but as repeated outcome. When coordination across a boundary is expensive, the boundary shows up in the codebase.
Researchers use socio-technical congruence for the fit between who must coordinate and what the code depends on. Plain version: do the people who need to talk actually talk before they edit the same modules? When coordination patterns match coordination needs, modification requests resolve faster. Misalignment is measurable drag — latency and rework piling up where the org chart and the dependency graph disagree.
The mirror runs both ways. Vivian Bellotti, revisiting Conway for modern engineering organisations, describes managers subdividing teams before the work justifies the split — because career ladders reward running more units. Architecture gets over-partitioned to broadcast importance. Scaling the organisation ahead of need resembles scaling microservices ahead of need: both restrict future choices.
A reorganisation that dissolves the warehouse team but leaves the WarehouseSync package untouched manufactures new legacy on day one. The code still runs. The social structure that explained the module's seams is gone. Congruence research describes this tension as a rubber band: social and technical structures pull on each other over time. Cut one side without adjusting the other and something stretches until it snaps — usually in production, usually on a change that "should have been simple."
The risk register legacy teams actually need
Age belongs on a slide. Delivery needs a register. Score candidate systems on three signals — not to win an argument in architecture review, but to decide where the next week of attention earns interest reduction rather than motion.
- Signal 1 — Change friction — Tests (or their absence), coupling that turns a one-line fix into a five-file excursion, deployment steps nobody has run in a year. Feathers' proxy applies here: if you cannot verify safety cheaply, friction is already high.
- Signal 2 — Knowledge concentration — Bus factor. Runbook gaps. Git blame with one dominant author on load-bearing paths — and when that engineer retires, the runbook often retires too, even if the binary keeps running.
- Signal 3 — Socio-technical drift — Module ownership that no longer matches who must coordinate to ship a change; remote execution without local decision rights; community smells that predict duplicated logic.
Consider the reconciliation job from that upgrade thread. Fifteen years in production. Zero automated tests on the rounding rules. One author due to retire in nine months; the margin notes explaining tax exceptions live in a personal notebook. Package boundaries still follow a warehouse organisation dissolved in last year's reorg — InboundAdjustments owned by a team that no longer exists on the chart.
On age alone, the job is legacy by procurement standards. On the register, change friction is critical — any rule tweak is manual verification or prayer. Knowledge concentration matches it: the notebook is the spec. Socio-technical drift is worse still; the ownership map is fiction.
A proposal to modernise the stack misses the blocker because it scores vintage, not signals. The runtime is not the bill. The unpaid social and technical loans are.
A rewrite without tests, shared runbooks, and clarified ownership recreates the same register in a new repository — often with worse history and the same community smells.
Rescue beats rewrite when the social bill is unpaid
Cunningham argued for incremental growth with consolidation: pay debt in the modules you touch, rewrite when understanding catches up. That is slower than a greenfield promise. It is also how most businesses stay running while they improve.
The fair objection is that some estates are too far gone to rescue incrementally. That objection is right about the extremes. Where it errs is in treating install date as proof of extremity. Big-bang replacement earns its place when the social structure is being redesigned alongside the code — when decision rights, ownership, and architecture are negotiated in the same programme, not assumed to follow from a new repo. It also earns its place when regulatory boundaries have shifted beyond patch scope, or when verifying legacy behaviour costs more than a bounded replacement with traced requirements. That outcome belongs on the register; it is not the default procurement reaches for when a slide shows an old date.
Before the author leaves, document what they know in forms the next engineer can falsify: characterisation tests on the reconciliation outputs that matter, a runbook that names the three external feeds and what "late" means for each, an ownership line on the dependency graph that matches who can actually approve a schema change. Philippe Kruchten and colleagues note that technical debt management fails when only engineering understands the cost. The register is a shared language for that conversation — not a stick to wave at the team that inherited the mess.
Most legacy programmes do not need a hero architect. They need an honest register of where change hurts, a runbook someone else can execute, and one consolidation sprint that proves the team can still move a date without flinching.