Picture this: you join a new team working on a big system. Everybody who knew anything has left, either to find greener grass or to enjoy a well-deserved pension. You and the team struggle to build new features for the system or to adapt functionality to match changes in legislation. Not to mention the trouble it is to figure out what to fix when things go wrong. At the same time, the business that you support is screaming for innovation and pushing for more and more changes.
Recognize this situation? Ever experienced it yourself? If you’ve been building software for more than a few years, I bet you have.
A world full of legacy systems
“Legacy. What is a legacy? It’s planting seeds in a garden you never get to see.”
– Lin-Manuel Miranda, “Hamilton”
Legacy, the thing that you are remembered for, typically a word that has a positive meaning.
How come that in tech the word “Legacy” has such a bad connotation? When we call out a legacy system, we usually mean: code without tests (Michael Feathers) or code you “got” from somebody else, or code that you’re scared to touch.
However, there is a reason these legacy systems are still around. In almost all cases, that system still brings in money or is somehow still valuable. If it did not bring any value anymore, wouldn’t it be decommissioned? There must be something to these systems that makes them survive, where other systems did not.
How systems become “Legacy”
So legacy systems are those that have become hard or scary to change. In my experience, that is not because something is wrong with the code or technology. The major contributing factor is usually that the knowledge about the system has left the organization. And then I don’t mean the documentation, but the people that built, maintained and ran the system. When those people are gone, you know that nobody else is going to be happy touching that thing.
The value of software
What is code? Code is like a mapping of desired real-world behavior to a program that can be executed by a machine. Code is crucial to a system, but is the code the most valuable part of a system?
Over the past years I’ve talked a lot at conferences and training workshops about the value of code. How I used to explain it is that the value of software is to be easy to change. It’s in the name: soft = flexible. Which contrasts with hardware, which is quite hard to change once you’ve soldered everything together. Pretty much all the things that we do to improve our way of writing code boil down to making the code easier to understand for the next person working on it (or yourself in 6 months). Our primary objective for building maintainable systems is to make code easy to change. In a sense, to make the “legacy” that we leave behind be a positive one.
Then, about a year ago, I got drawn deep into the rabbit hole by this video on The Value of Source Code by Philomatics. It pointed out that the value of a software system does not have a lot to do with the actual source code. Instead, the real value is the ability to quickly change the system, and that the key thing you need for that is people with a “mental theory” of why the system works the way it does. Without that theory, no person is able to confidently make any changes.
For me, this way of looking at it immediately made sense. When applied to my own observations on the numerous software systems I worked on over the years, everything just fell into place. I also realized that this fundamental shift in thinking about software systems has wide implications. Or as Maria Rey puts it so adequately:
“This theory is learned through immersion, through building and debugging and negotiating constraints. You cannot download it; you acquire it by living with the system until the system inhabits you. This is an uncomfortable idea, because as soon as we say this, the economic picture around code trembles: the most valuable part of the program, the part that determines whether it can be safely changed, does not appear on the balance sheet. It is invisible, embodied, and therefore precarious.”
– Maria Rey
That all brought me down the track of diving into this topic, talking about it at conferences and writing this blog.
Programming as Theory Building
This is not some new AI-era insight either. Peter Naur, the N from BNF and a Turing Award winner for his work on Algol 60, already wrote about this in 1985 in his article “Programming as Theory Building”.
Naur’s point is simple, and once you see it you cannot really unsee it. A program is not just the source code sitting in a repo. The real thing is the theory people build in their heads while working on it. Why it works that way. Why it was designed that way. What tradeoffs were made. What parts are safe to touch and what parts will bite you.
What made that argument so sharp is the world he was arguing against. The dominant view back then treated programming as a kind of production process. Get the requirements in, have programmers translate them, get code and documentation out. In that picture, the human beings in the middle are mostly interchangeable. If the specs are good enough, anyone should be able to pick up the work and continue.
Production View
Input = Requirements & Observing real world
Programmers = Replaceable "machines"
Output = Code & Docs

What Naur saw, and what I keep seeing on teams now, is that this is just not how software creation behaves in practice. Programs are human constructions. The important part is not only the artifact, but the understanding wrapped around it. Specs help. Docs help. Tests help. But none of them carry the full story on their own. They only really work when people still understand what problem the system is solving and why earlier decisions were made.
Theory-Building View
Input = Observing real world, situational experience
Programmers = Build a mental theory
Output = Theory: mapping between real world & code
Code = Side-product
This was not just philosophy for Naur. He got there by watching real projects. And that part matters, because the production view sounds reasonable right up until you have to live inside an actual system. On paper, changing software looks cheap. You edit some text. You run the build. You ship.
In real teams, the expensive part is figuring out whether the change is actually safe, whether you really understood the situation, and whether you understand what side effects will occur.
When he applied the theory-building view to his observations, the actual outcomes matched exactly with what the view would predict. Software transferred from one team to another with code, docs AND human consulting resulted in success. A similar transfer took place a while later. This time, only the code and docs got transferred, WITHOUT human contact between the teams. The result: an unmaintainable system.

The effect was not only visible in the ability to change a system, he also saw it around the ability to deal with unexpected situations. I advise you to read Naur’s original text for more details on what he described.
As I already mentioned, to me this immediately clicked. Having people on the team that deeply understand a system makes a huge difference for how effective the team is going to be at adapting it and dealing with incidents. I’m sure that anyone with sufficient experience on software projects will feel the same.
Can’t we just document the theory?
Ok, so if that mental theory is so important, why don’t we just write it down in documentation?
Seems like the obvious answer. The trouble is: you can’t.
Writing down your mental model of a system is the same as asking someone to write down how to ride a bike and then asking someone else who never rode a bike to learn it from the text. This is the nature of Tacit Knowledge, it is learned by doing, not by reading. When you learn cycling, you “build a feel” for the balance. Reading an essay on how gravity works is not going to help you. The same goes for someone learning the finer touches of playing a violin, or the intuition on how to react that’s built up by basketball players.

| Tacit Knowledge | Explicit Knowledge | |
|---|---|---|
| Definition | Personal, experiential know-how that’s hard to articulate. | Formal, documented knowledge that can be written down, codified, or stored. |
| How it’s gained | Through practice, intuition, trial-and-error, observation. | Through reading, lectures, databases, manuals. |
| How it’s shared | Mentoring, storytelling, shadowing, demonstration, practice. | Books, reports, procedures, checklists, training materials. |
| Examples | Riding a bike, a chef’s “feel” for seasoning, a leader’s intuition in a crisis. | Recipes, standard operating procedures (SOPs), user manuals, financial reports. |
| Transfer difficulty | Hard to capture in words and usually requires personal interaction. | Relatively easy. You can just copy, store, or transmit the written or digital record. |
| Value | Enables creativity, innovation, and expert judgment. | Provides structure, consistency, and scalability. |
The theory of a system is tacit knowledge. This is “The Art” of doing something.
The code, tests and the docs are explicit knowledge. These are “the instructions” for doing something.
“The Art” of doing something is not something you can write down. It must be passed person-to-person and learned through experience and social interaction.
“The conclusion seems inescapable that at least with certain kinds of large programs, the continued adaptation, modification, and correction of errors in them, is essentially dependent on a certain kind of knowledge possessed by a group of programmers who are closely and continuously connected with them.”
– Peter Naur, Programming as Theory Building
The theory-building view states that the continued value of a software system requires a living mental theory with the people working on that system. Yet the dominant production view completely ignores that, unknowingly causing systems to decay when that mental theory is lost.
So what survived from the age of Naur?
Ok, nice theory from the 80s, why doesn’t anybody seem to talk about this and why does it seem like our industry is just completely ignoring these quite fundamental insights?
Looking back, our industry never fully chose one side. We kept a lot of the formalist instinct, and that is not a bad thing. We like tests, contracts, static analysis, CI, typed code, all of that. At the same time, a lot of the ways good teams actually work came much closer to Naur. Pairing. Reviews. Domain conversations. Keeping developers close to incidents and operations. All of that only makes sense if human understanding is part of the product.
| Adopted from formalism | Adopted from Naur |
|---|---|
| Production View | Theory-Building View |
| Specs, contracts, testing. | Recognition that human and social factors are critical. |
| Though less mathematical than envisioned | Agile, DevOps, Domain Driven Design, Sociotechnical engineering, pair programming, code reviews |
| Emphasis on rigorous tools and processes. Static analysis, type systems, automated testing, CI/CD pipelines | Documentation is seen as secondary to “working software and team communication”. Straight from the Agile Manifesto. Alistair Cockburn even included Naur’s article in his book “Agile Software Development”. |
So some of the thoughts of Naur made it through, but clearly not enough. After all, we’re looking at software creation as a production pipeline (we literally call it that) and we’re seeing way too many places where teams operate as “feature factories”.
What this means in the age of AI
The obvious pushback is that that was the 80s and we now live in a very different world. AI can regenerate code quickly, so readable code matters less nowadays, right?!
We live in a world that is under constant change. The real value of software is not preserving source files, but preserving the ability to adapt behavior when the world changes (which it will). AI helps us immensely in producing code quicker, but it does not remove the need for a human theory of why the system exists, what tradeoffs were made and what must not break. There is another problem here. Early research suggests that when AI is heavily used to generate code, developers get fewer of the small, hands-on interactions through which real understanding is built. They still see the system, but more from the outside, and that makes it harder to develop the deep mental theory needed to change it with confidence.
Ok, all fine, but we are doing spec-based development now. So all these things are nicely written down in specs and if the code gets ugly, we can just regenerate the complete system from those specs, right?!
Well… the trouble is that the mental theory, the “art” of the system, cannot be fully written down. Specs are incomplete artifacts too. Just getting all the relevant expert understanding from the right people into a spec is hard in the first place. And even then, specs still need interpretation. Code has a strict syntax. Specs are written in natural language, and natural language is inherently ambiguous. The hardest part is usually not writing the code from the spec, but knowing which spec is wrong, stale, underspecified, politically negotiated, or missing.
Rewriting a codebase every three months is only safe if the team is able to review it, which requires understanding the domain and the consequences of any change. AI makes syntax cheap. It does not make judgment cheap.
If anything, faster code generation increases the premium on shared understanding, because a team can now produce more change than it can truly comprehend. I’m hearing some people call this “cognitive debt”, but that suggests a conscious tradeoff. What we are really seeing is unintentional cognitive decay, and if we keep using AI the way we are now, that decay will rise at an alarming rate.
Knowledge walking out the door
It is clear that the survival of human mental models in a team is way more important than we treat them in today’s industry. Where Naur warned us that programmers are not replaceable cogs in a machine, the current industry trend is to treat developers as just that: code monkeys that can be replaced with cheaper AI.
At the same time, developer job-hopping every few years has been a reality for quite some time. I was curious what causes lie behind that, to help figure out what we can do about this. What I found is quite shocking.

I couldn’t find any definitive source, but taking numbers from various surveys and reports, the following causes jump out:
- Career growth / Desire for new challenges To me this feels like the most logical and common reason I hear for people switching jobs. With percentages between 10 and 20, reality seems that this accounts for a fairly low percentage.
- Poor leadership These have me rather worried. Management and poor leadership showing up at roughly the same level as career growth. Clearly a topic that requires attention.
- Burnout / Overwork The numbers for this one vary wildly and the high end is absolutely alarming. That this one is even in here is not ok.
- Aging / Retirement Fairly low numbers here for the moment, but this is a demographic train coming our way, and no way to stop it. A lot of knowledge about the systems running today are in the heads of the more experienced engineers in our organizations. When they leave, their knowledge leaves with them, and as we already concluded, no amount of documentation is going to prevent that.
- Technical Debt / Legacy These are the most intriguing numbers for developer turnover. Rather high, which, to be honest, makes total sense. If a system is already hard to work with, the people on the team feel that pain every day. Every change takes longer than it should. Every incident takes too much energy to solve. And the less confidence people have in the system, the more stressful the work becomes.
How legacy feeds itself
That last one is where the vicious circle kicks in. Legacy systems drive people away. When people leave, their theory of the system leaves with them. With less theory on the team, the system becomes harder to change. A harder system drives more people away. And the cycle repeats, each turn making the system a little more opaque, a little more fragile, a little more “legacy.”

Legacy, then, is not just technical decay. It is a self-reinforcing loop of knowledge loss. It is what remains when the system outlives the shared theory that once made it changeable.
And that also explains something important. When engineers avoid touching certain parts of a system, it is usually not because they are lazy or unwilling. It is because nobody can still explain with confidence what those parts are really doing, why they were built that way, and what might break when you change them.
Why keeping knowledge alive is getting harder
That leaves me with a bigger question. With all the tools, all the process improvements, all the focus on documentation and collaboration, why does knowledge continuity seem more fragile now instead of less? The vicious circle of legacy is already bad enough on its own. But there are forces outside of it that are accelerating the loss of theory even further.
Post-COVID, the continued normalization of remote work has removed many of the small, informal moments where tacit knowledge used to spread: overheard conversations, quick questions, spontaneous pairing, the little bits of context you absorb just by being around people who know the system well.
At the same time, AI-assisted delivery lets us produce more code, faster, while reducing how much understanding humans are forced to build along the way.

The theory lives in human minds. Our tools increasingly let us produce vast amounts of code without building understanding. What exactly are we optimizing for? Fast delivery now, unmaintainable systems in two years? Is that the bargain we are making without realizing it?
So what now?
Enough about the problem at hand. I’m a practical guy, so I propose we don’t let that happen. AI is a great tool, and at the same time we have some serious work ahead of us to prevent uncontrolled cognitive debt. I’ve been working on ways to actively counter this, some of which are covered by the talks I’ve done on this topic. I’m drafting a blog on those too, so check back here in a few weeks if you’re curious.
