A particular kind of dread settles over a business when the answer to “can we change this?” becomes “only Dave knows, and Dave left in 2019.” The system still runs. It still takes the orders, calculates the prices, sends the invoices. But the people who built it are gone, nothing is written down, and so every proposed change is met with the same flinch. Nobody is confident enough to touch it, which means the system quietly starts dictating what the business is allowed to do.
It is worth being precise about what makes this dangerous, because the danger is not age. Old code that is well understood and well tested is perfectly safe to change. The danger is the specific combination of three things: nobody can fully say what the system does, the rules that matter are buried in the code rather than written down, and there is no test suite to tell you whether a change broke something. That combination is what makes change feel like a gamble — and it is also, fortunately, a combination you can dismantle in a deliberate order.
Why the business rules went missing
Most established systems are not the system anyone designed. They are the accreted version: the original idea, plus a decade of patches, exceptions, integrations and “just get it working before month-end” fixes, layered on by people who have mostly moved on. The architecture became an emergent property of the change history rather than a deliberate design — what practitioners call accidental architecture.
In a system like that, the real business logic frequently exists nowhere except the code. The rounding rule that keeps the accounts balancing. The eligibility exception for one large customer. The special case nobody remembers the reason for but that breaks the warehouse if you remove it. As one practitioner analysis of legacy systems puts it: in legacy systems, correctness is almost never written down; requirements live in tribal knowledge, and the code itself does not explain intent — it only reveals behaviour after the fact. The behaviour is the specification, and the specification is unread.
This is exactly why “we’ll just rebuild it” so often goes wrong. A rebuild captures the obvious eighty per cent and silently drops the exceptions the business actually depends on. The new system looks right, demos beautifully, and then fails in production on the edge cases nobody knew were load-bearing. You cannot safely replace what you cannot yet describe.
The way out: discovery before change
The instinct, faced with an opaque system, is to start changing it carefully and see what happens. That is the most expensive possible approach. The disciplined route is to understand first, in three deliberate stages, before altering a single line.
- Map the system. Establish what the application actually does end to end, what the major parts are, and how data flows through it. The goal is to turn “scattered files and tangled dependencies nobody understands” into a navigable picture a new person could follow.
- Recover the business rules. Find the rules hiding in the code — the calculations, eligibility conditions and exceptions — and bring them into the open as explicit, written statements a domain expert can confirm or correct. This is where tribal knowledge becomes a documented specification.
- Pin the behaviour with tests. Before changing anything, write tests that capture what the system does today, so you have an objective record of current behaviour and an alarm that sounds the moment a change alters it.
That third stage is the keystone, and it has a name. Michael Feathers calls these characterisation tests, and describes them with deliberate plainness: a characterisation test captures the actual, current behaviour of the code — warts and all — rather than asserting what it ought to do. You run the program, capture what it produces, save that as the expected result, and from then on any change that alters the output sets off an alarm. Paul Jones once described the idea even more bluntly: the stupidest possible test — run it, copy the output, save it, change the code, run again, and check the output is the same.
The test is a way of grounding our knowledge of a system.
That is the whole point. The tests are not there to prove the code is good — they are there to ground what you know about it, so that change stops being a gamble. With a net of characterisation tests in place, you can begin to untangle, document and improve the system in small, reversible steps, confident that the moment you accidentally change behaviour, you will be told. Without them, you are working blind. With them, the same system becomes ordinary to work on.
Where AI genuinely helps — and where it must not
Discovery is slow, expensive, manual work — reading unfamiliar code line by line, tracing what calls what, reconstructing intent from behaviour. This is the part of modernisation where modern AI tooling earns its place, and it is the lowest-risk, highest-value use of it. AI is good at reducing the friction of initial comprehension: summarising what a module appears to do, explaining unfamiliar idioms, translating low-level detail into higher-level intent, and drafting the first version of a map or a test far faster than a person reading cold. It turns weeks of orientation into days.
But there is a hard limit, and respecting it is the difference between modernisation and a new mess. AI output is plausible, not necessarily correct. It can confidently describe a rule that does not exist, point an engineer at the wrong file, or — most dangerously when generating tests — read a buggy line of code and write a test that enshrines the bug as intended behaviour. A green dashboard is not the same as a true safety net. Feathers, who has carried his testing discipline into the AI era, is careful about this: the hallucination problem is real, so the validation has to happen in the work itself; you do not paste generated output straight into a system and trust it.
We know that the hallucination problem is real with this technology, so as a result we have to be cautious… the validation can occur in the work when you do it.
So the rule we hold to is simple: AI output is a hypothesis about the system, never a fact about it. Every map it draws, every rule it claims to have found, every test it writes is treated as a lead to confirm against the running code and a domain expert — not as truth. Humans decide what matters; AI helps with the labour of finding it. Unverified AI output, plugged in and trusted, is just new undocumented code generated at speed — the very problem you set out to escape. Used with that discipline, AI makes discovery dramatically faster without making it reckless. That is what moving beyond vibe coding means in practice.
Frequently asked
- Is undocumented code always a problem?
- Not on its own. The danger is the combination of no documentation, no tests and concentrated knowledge — when nobody can say what the system does and nothing can prove a change is safe. A well-tested old system without much documentation is far safer to change than a fragile new one without tests.
- What are characterisation tests?
- Tests that capture what the system does right now — exactly as it behaves today, warts and all — rather than what it should do. You run the system, record the output, and from then on any change that alters that output is flagged immediately. For legacy code with no tests, they are the safety net that makes change safe.
- Why not just rebuild it from scratch?
- Because the real business rules usually live only in the existing code, and a rebuild tends to capture the obvious behaviour and silently drop the exceptions the business actually depends on. Without first recovering those rules, a rewrite often reproduces the easy parts and loses the important ones.
- Can AI just document and test the system for us?
- AI can accelerate it substantially — reading code, drafting maps and tests — but its output is plausible, not guaranteed correct, and it can confidently invent rules or write tests that confirm existing bugs. Every output must be validated by an engineer against the running system. AI does the labour; humans keep the judgement.
If your system has become something nobody fully understands, the first move is not to start changing it — it is to make it knowable again. Our free assessment at /application-modernisation/free-assessment begins exactly there: mapping how your system really works, finding where the important rules and risks live, and showing you what documentation and tests would make change safe — using AI-assisted discovery with an engineer checking every output. Understanding first is what makes everything after it cheaper and far less frightening.