The wiki is three refactors out of date, the architecture diagram in Confluence describes a system that stopped existing years ago, and the two engineers who understood the billing module left before you arrived. This is the normal starting condition for legacy application modernization, not the exception. Every long-lived system ends up here, because documentation decays and running code does not.
So the honest question is not “where are the docs” – they are gone, and waiting for them is how projects stall for a quarter before a line is touched. The question is how you recover enough truth to change the system safely. That truth is already in front of you, in the traffic it serves, the logs it writes, and the calls it makes at runtime. This article is about reading it.
Why documentation rots
Documentation rots for structural reasons, not because a past team was lazy. A document is written once, then competes with every deadline that follows: the code gets the hotfix at 2am, the wiki does not. Multiply that by a decade of turnover and the paper trail is not merely incomplete but misleading – confidently describing behaviour the system abandoned years ago.
The worst of it is tribal knowledge: the reconciliation rule that lives only in a senior analyst’s head, the “don’t touch that batch job on the 1st” that everyone knows and nobody wrote down. When those people leave, that knowledge stays encoded in one place only – the behaviour of the system itself, still running, still correct, and unable to explain itself.
The runtime canon
The authoritative record of how a system actually behaves, encoded only in the running production system – present in no document and no living person’s head. When the wiki, the original author, and production disagree about what happens on a leap year, production wins, because production is the canon. Modernisation is the work of transcribing that canon into something a team can read, test, and change.
One qualifier. If your system is small, a few thousand lines, and the person who wrote it still sits two desks away, none of this applies – read the code, ask them, and move on. You do not have a discovery problem, you have an afternoon. This is for systems too large and too old for anyone to hold in their head.
Start legacy application modernization with discovery, not documentation
The first move is to observe the system doing its job, not to reconstruct its history. Four techniques recover most of what you need, and you can start all four on Monday with tools you already have. Run them in parallel; each covers the others’ blind spots.
Production traffic analysis
Point your attention at the front door. Which endpoints actually get hit, how often, and by whom? A week of access logs or gateway metrics shows that most of a system’s surface area serves a handful of routes, and that whole modules the diagram treats as central receive no traffic at all. That is how you find the critical path empirically instead of guessing – and the dead code you can ignore.
Log mining
Logs are a diary the system keeps about itself, and it does not lie about what it did. Read the sequences: what order things fire in, where retries cluster, which error appears every night at the same time. Business logic that appears in no specification is often plainly visible in the log stream – a validation that always precedes a write, a fallback that triggers on a specific input. You are reverse-engineering the rules from their consequences.
Dependency tracing
Map what calls what at runtime, not what the architecture claims. Static diagrams describe intentions; live tracing describes reality, and the two diverge badly in old systems. A service the diagram shows as isolated turns out to reach into a shared database three other services also write to. That undocumented coupling, the call nobody remembered was there, is exactly what breaks a modernisation when you move the wrong piece first.
Structured interviews with the operators
The people who run the system daily hold context no log can give you – the reason a job runs at an odd hour, the input that must never be trusted. Interview them with structure: walk a real transaction end to end, ask what they do when it goes wrong, ask what they are afraid to touch. “What breaks that you have learned to work around?” surfaces more truth in ten minutes than a week of reading. Do it early, while the people are still reachable.
The four discovery techniques, what each one recovers, and where each one goes blind.
| Technique | What it reveals | Blind spot |
|---|---|---|
| Traffic analysis | The routes actually used, the critical path, the dead surface you can ignore | Rare but critical flows, month-end, disaster paths, that barely register in a week |
| Log mining | Real call sequences, hidden rules, the errors the system quietly recovers from | Anything the system never logged, and logic that leaves no visible trace |
| Dependency tracing | The true runtime coupling, including shared state and calls no diagram shows | Intent – it tells you what connects, never why it was built that way |
| Operator interviews | The why behind the workarounds, and rules that live only in people’s heads | Memory is partial; people describe the system they believe they run |
The only documentation that never lies is the system itself.
Turn discovery into a living dependency map
Discovery findings are worthless if they rot as fast as the docs they replaced, so do not produce another one-time diagram destined to be wrong within a month. Produce a dependency map that lives in version control next to the code, is short enough that keeping it current is cheap, and gets updated in the same commit that changes what it describes.
Do not try to map everything. A complete map of a large legacy system is a multi-month project that is out of date before it finishes and that nobody reads. Start with the critical path you found in the traffic – the flows that carry the real load and risk. Map those, mark the boundaries where they touch systems you do not yet understand, and leave the rest as honest blank space. A map that admits what it does not cover beats one that pretends to cover it all.
Document as you touch it, never in a sprint
A dedicated documentation sprint before modernisation is a trap that looks responsible and fails predictably. You spend weeks writing down your understanding at its shallowest, and the result is a fresh batch of documents that start decaying the day the real work begins. You have paid for docs and bought nothing modernised.
Document each component at the moment you touch it, and not before. When you migrate a module and characterise it with tests, your understanding is deepest and most tested against reality, so the note you write then is worth keeping. Accurate documentation falls out as a side effect of the work, covering what you genuinely changed and staying silent on the rest.
The rule
Never document a part of the system you are not currently changing. Understanding you have not verified by touching the code is a guess, and a written guess is worse than a blank page because the next person believes it.
What the tooling tells you in a Java and Spring system
In a Java and Spring codebase, the shape of legacy we most often inherit, a handful of tools turn each technique from manual archaeology into something repeatable. Reach for them in this order; each answers a different question about the runtime canon.
- Spring Boot Actuator: the fastest way to make a running application describe itself – the live bean graph, active configuration, dependency health, and the mappings it really serves. That is runtime truth, not what the source suggests. Reach for it first.
- OpenTelemetry: your dependency-tracing instrument. It follows a single request as it threads through tangled service layers and out to databases and queues. Reach for it to see what actually talks to what under load, and to catch the coupling no diagram admits to.
- jQAssistant: a structural scanner that reads the whole codebase into a queryable model, so you can ask which packages depend on which, where the cycles are, what reaches into a module it should not. Reach for it when the structure is too large to hold in your head.
- ArchUnit: once discovery has taught you the boundaries that matter, it turns them into checks that run with your build and fail the moment someone crosses a line. Reach for it to stop the system drifting back into the tangle you are climbing out of.
None of these tools understand your business rules; they map structure and behaviour, not meaning. They tell you what the system does. The people you interviewed tell you why it was ever supposed to.
When part of the system stays a black box
At some point discovery hits a ceiling. A module resists every technique – no useful logs, no reachable author, behaviour too tangled to trace with confidence. Accept it. Chasing full understanding of every corner is how modernisations run out of budget before they ship. The mature move is not to understand the black box but to build around it so its mystery cannot hurt you. Three risk-bounded strategies do that, and they work together.
- Strangler fig to isolate the unknown: Martin Fowler’s pattern wraps the black box and routes new work to new code, letting the old module run untouched behind a boundary until you can replace it – or decide you never need to. You quarantine what you do not understand instead of gambling on a rewrite.
- Contract tests at the boundaries: pin down how the black box behaves at its edges, what it accepts, what it returns, how it fails, and lock that into tests. You need not know how it works internally, only that it keeps honouring the contract everything around it depends on.
- Feature flags to limit blast radius: when you finally change something near the unknown, put it behind a flag, roll it out to a sliver of traffic, and pull it back in seconds if an assumption was wrong. The flag turns a bet into a bounded experiment.
This is how we replaced a live Java monolith without a big-bang cutover – the full account is in how we replaced a live Java monolith with the strangler fig pattern, with the wider sequencing in our guide to modernising legacy systems without downtime.
Where this leaves you
None of this is theory. When Mediacom came to us, a weekly report ran on three siloed data sources reconciled by hand – no shared schema, no API, no source of truth, no audit trail on numbers that reached the board. The reconciliation rules existed only in the analysts’ heads and their spreadsheets: a textbook runtime canon, no documentation to inherit.
We did not wait for a specification that was never going to exist. We recovered the rules from how the work was actually done, encoded them, and caused zero disruption to the production system. Twelve weeks later the weekly report had gone from four days of analyst work to one – the full story is in the Dango case study.
Missing documentation is not the blocker it feels like on the first morning – only if you treat writing the docs as the prerequisite instead of the by-product. Legacy application modernization starts with observation, then proceeds one changed, characterised, quarantined component at a time. The system tells you the truth constantly; the work is learning to listen.
If you are staring at a system nobody documented and wondering where the first cut goes, that is the conversation we have most weeks. See how we approach it on our legacy modernization page, or read why the alternative so often fails in why most legacy rewrites fail.
Tell us what your system does that nobody can explain – that is usually where the first slice of work reveals itself.
