How to recover knowledge from legacy code after its author leaves
Learn how to recover knowledge from legacy code using source analysis, production evidence, parity tests, and honest records of what cannot be restored.

When the author of an old system leaves, the knowledge does not vanish in one clean event. Some of it remains executable in source code. Some survives only in production data, schedules, operator habits, and integrations. Some was never recorded anywhere and is gone. A responsible reconstruction separates those classes instead of pretending that a sufficiently clever code review can recover them all.
The aim is not to explain every function. It is to rebuild an evidence backed account of what the system does, which behavior the business depends on, and where uncertainty remains. I have seen teams waste months annotating dead branches while the actual contract lived in a nightly file drop and an accountant's spreadsheet. Start with evidence, preserve contradictions, and make the replacement prove equivalence where equivalence matters.
Code tells you mechanism, not the whole contract
Source code can reveal control flow, data transformations, validation rules, calculations, message formats, database access, and the sequence of calls to external systems. It can often answer precise questions: Which status values stop billing? How is interest rounded? Which fields make a record eligible for export? Where does a retry stop? Those are facts worth extracting automatically.
Code cannot, by itself, tell you whether an observed rule is current policy, an obsolete workaround, or a bug that users learned to accommodate. A branch that gives customer class 17 a different tax treatment proves that the program does so. It does not prove why class 17 exists, whether the exception remains lawful, or whether anyone still creates those customers. Comments rarely settle the matter. They may describe the rule as intended when written, while production has followed a modified path for ten years.
Keep three terms separate. Implementation is what the source can execute. Observed behavior is what the deployed system actually did under particular inputs and conditions. Business intent is the reason someone wanted that outcome. A rewrite needs the first two to preserve service, but only people, policy records, contracts, or contemporaneous decisions can establish the third. Confusing intent with implementation turns accidents into requirements. Ignoring observed behavior breaks consumers that depend on those accidents.
This distinction also stops a common argument about whether the code is the specification. The code is authoritative about its possible instructions, subject to configuration and runtime dependencies. Production evidence is authoritative about which possibilities occurred. Neither source proves what ought to happen next year. Treat each claim with a provenance label instead of forcing one artifact to answer every question.
Build an evidence map before interpreting the program
An evidence map should identify every place where the system's behavior can be observed or constrained before anyone starts writing prose documentation. Otherwise the easiest repository becomes the center of the investigation, even when the consequential behavior sits outside it.
Inventory the evidence in four groups:
- Executable material: source, build scripts, JCL, stored procedures, report expressions, spreadsheet formulas, generated code, and deployed binaries.
- Runtime material: configuration, feature flags, scheduler definitions, environment variables, database schemas, queues, file layouts, and service endpoints.
- Observations: request logs, message samples, batch inputs and outputs, database changes, printed reports, failure tickets, and operator runbooks.
- Authority records: contracts, policy manuals, regulatory interpretations, approved change requests, and decisions from people who own the process.
Record origin, date range, environment, owner, retention, and known gaps for each item. A production log without its configuration version can mislead you. A database copy without a business date can make end of period logic look random. A source tree without the deployed binary cannot prove that the repository matches production.
Use a small evidence ledger rather than a giant narrative document:
claim: invoices with hold_code R are not exported
status: observed
evidence:
- export_job.cob lines 1840-1868
- nightly output sample 2024-01-16
- scheduler definition AR_EXPORT
contradiction:
- runbook says only hold_code L blocks export
owner_needed: accounts receivable
confidence: medium
The contradiction is the useful part. Do not resolve it by picking the newest file or the most confident person. Reproduce the input, trace the branch, check historical outputs, and ask the process owner whether the discrepancy reflects policy or drift. The ledger makes that dispute visible and gives a later reviewer something falsifiable.
Before touching production data, define access and handling rules. Traffic captures may contain credentials, personal data, payment details, or confidential free text. Minimize fields, redact consistently, keep raw evidence access narrow, and retain a mapping only when replay truly requires it. Reconstruction does not excuse creating a second uncontrolled archive of sensitive records.
Static reconstruction starts at system boundaries
The fastest way to understand an unfamiliar system is to map what crosses its boundaries, then trace inward. Starting at the main entry point works for small programs. In a mixed estate of COBOL, JCL, PL/SQL, desktop code, and scheduled scripts, there may be no honest single entry point.
Extract interfaces first: files read and written, tables touched, messages consumed, HTTP routes, terminal screens, command arguments, printer reports, and scheduled job names. For each boundary, capture the schema, caller or recipient, timing, error behavior, and the code path that handles it. This produces a dependency graph grounded in real inputs and outputs rather than a diagram based on folder names.
Automation can do much of this. Parsers can build call graphs and data lineage. SQL analysis can map reads and writes. Constant extraction can find status codes, date masks, record types, queue names, and file paths. Cross language symbol resolution can connect a JCL step to a COBOL program, that program to a stored procedure, and the procedure to a table. Duplicate condition detection often exposes the same business rule implemented differently in several channels.
Search results are leads, not conclusions. Dynamic dispatch, reflection, generated SQL, copied source members, preprocessor directives, and runtime configuration all weaken a static graph. A call graph also says nothing about frequency. A branch executed for every order and a branch last used during a discontinued migration can look equally important. Mark unresolved edges and measure them later.
Repository history helps when it is genuine history rather than a bulk import. These commands produce a compact trail for a suspicious rule:
git log path/to/export.cob
git blame -L 1840,1868 path/to/export.cob
git show <commit>
The useful output is a sequence of commits, authors, dates, and changed paths. Read the associated issue or change request if it exists. Do not infer business intent from an author's name or a terse commit message. A line blamed on a migration commit may be decades older than the repository.
IBM's JCL documentation treats DD statements as the association between a program's logical data name and an external data set or device. That is a good example of why boundary analysis matters: reading the COBOL SELECT alone does not tell you which production data set arrives in that slot. You need the deployed JCL, catalog conventions, scheduler parameters, and sometimes the operator procedure to reconstruct the actual input.
Production traffic reveals the de facto contract
Recorded production interactions show which inputs occurred and which outputs consumers received, including behavior nobody thought to document. They are the strongest practical basis for a parity harness, provided you understand what the recording excludes.
Capture at stable boundaries. For a service, record normalized requests, responses, status codes, and durable side effects. For batch, preserve input files, parameters, relevant starting rows, output files, reports, and database deltas. For a desktop application, record commands or user actions at the domain boundary rather than video pixels unless screen layout itself is contractual. Replace volatile values such as timestamps and generated identifiers with comparison rules, not arbitrary deletion.
A useful replay case contains enough context to explain a mismatch:
{"case_id":"export-00418","business_date":"2024-01-31","input_ref":"sha256:...","config_ref":"sha256:...","expected":{"records":418,"rejects":3,"total_minor_units":9021441}}
The hashes bind the case to immutable evidence without putting a full customer file in the test definition. The expected object compares business results rather than byte equality. If column ordering or fixed width padding matters to a downstream consumer, add that separately as a format assertion.
Sampling needs intent. Random traffic covers common paths but misses quarter close, leap days, retroactive adjustments, empty files, maximum field lengths, reversals, and rare error recovery. Build strata around business events and branch conditions. Keep normal cases because they reveal volume patterns, then add boundary cases from code analysis and incident history. Never claim complete coverage merely because a large capture replays cleanly.
Traffic also contains inherited defects. If the old service returns an incorrect status that a downstream job interprets correctly, changing it during a rewrite may cause an outage. Preserve the behavior initially, label it as a known defect, and schedule a coordinated change. Parity is a migration control, not a moral endorsement of every legacy outcome.
Michael Feathers describes characterization tests in Working Effectively with Legacy Code as tests that record what software currently does, rather than what someone thinks it should do. That principle fits reconstruction, but it needs one qualification: a passing characterization suite proves equivalence only for its chosen observations. It cannot recover cases absent from the sample or prove the current outcome is lawful.
Time, state, and operators create hidden behavior
Systems with batch schedules, accumulated state, or manual operations cannot be reconstructed from isolated request and response pairs. Their output depends on when a job runs, what came before it, and which intervention changed the state.
Month end is the usual trap. A calculation may consult a business calendar, process late arrivals, reopen a prior period, and then create balancing entries in a later step. Replaying the final input file against a clean database produces a plausible result that is still wrong. Preserve a sequence of state snapshots and events across the boundary, including scheduler time zones and holiday tables. Test a closed period, a reopened period, and a failed run resumed after partial writes.
Retries deserve their own model. A job may be technically safe to rerun only because an operator deletes a marker file first. A queue consumer may deduplicate within one process but duplicate work after restart. A stored procedure may commit every thousand rows, leaving a prefix complete after failure. Static analysis can find commit statements and markers; only run history and controlled replay reveal how the recovery procedure works as a whole.
Operators are part of the deployed system even when nobody intended that architecture. Interview them with concrete artifacts. Ask them to walk through the last failed run, show the command they used, explain which output made them suspicious, and identify the person they call before rerunning. General questions such as "How does reconciliation work?" invite tidy descriptions. A real incident timeline exposes checks and exceptions.
Turn those interventions into explicit workflow states. Record preconditions, command or screen action, authorization, expected evidence, and rollback. If the replacement automates the action, preserve the decision point and audit record rather than hiding it inside a retry loop. If a judgment cannot be automated safely, keep it as a named human task with enough context for a new operator.
Clock behavior needs direct tests. Identify local time conversions, daylight saving transitions, business dates, database server clocks, and files whose dates come from their names rather than contents. Freeze the clock in tests where possible. For the parity harness, normalize display timestamps only after verifying that ordering, cutoffs, and accounting dates still match.
Rare exceptions carry more risk than common paths
The least frequent branches often encode the largest financial, legal, or operational consequences. Static analysis finds them, but evidence ranking determines whether they are active requirements, dormant safeguards, or unreachable debris.
Start with conditions tied to large amounts, privileged actions, jurisdiction, customer status, manual overrides, data deletion, or irreversible external messages. Compare those branches against production counts and policy records. A zero count means "not observed in this window," not "unused." Seasonal rules and emergency procedures may be valid even when no recent trace contains them.
A familiar failure begins with an apparently dead branch. The replacement team sees no executions in ninety days, removes it, and passes every replay. Six months later an annual adjustment arrives with a transaction type created by a scheduler parameter. The old branch would split the amount across two ledgers and print an exception report. The new system accepts the record through its default path, so totals remain balanced while the allocation is wrong. Nobody notices until reconciliation against an external statement.
The correct decision would have combined four facts: the branch existed, the scheduler could still produce the type, an annual runbook named the report, and the observation window did not include the annual event. None alone proves current need. Together they justify a targeted test and a question for finance.
Do not respond by documenting every branch with equal effort. That recommendation is popular because it produces visible progress and tidy coverage percentages. It is wrong because a thousand low consequence getters can bury one dormant settlement rule. Rank investigation by impact, reachability, evidence conflict, and reversibility. Leave mechanically generated references for routine code; spend human attention where a mistaken inference would be expensive to undo.
Deletion needs an explicit standard. Remove a path only when you can show it is unreachable in the deployed configuration, obsolete by an authoritative decision, or safely contained behind monitoring and rollback. Otherwise preserve it in the first replacement or quarantine it with a clear trigger. Uncertainty should affect the migration design, not disappear from the documentation.
Some knowledge is genuinely lost
No reconstruction method can recover an undocumented reason that left no distinct trace. If two business rationales would produce identical code, data, and outputs, evidence cannot tell you which rationale the author held. Claiming otherwise is storytelling.
Lost knowledge often includes rejected alternatives, political constraints, verbal promises, interpretation of ambiguous regulation, and the reason a threshold has a particular value. You may recover the threshold exactly and find every transaction it affected, yet still not know whether it came from law, risk appetite, a vendor limit, or a temporary concession. That distinction matters when someone proposes changing it.
Classify unknowns rather than smoothing them into confident prose:
- Recoverable: evidence exists but has not been connected, such as an unexplained column populated by a known job.
- Testable: intent is unknown, but current behavior can be measured and preserved.
- Decidable: evidence cannot settle the question, so an accountable owner must choose future policy.
- Irrelevant: the answer would not change behavior, risk, operations, or the replacement design.
For a decidable unknown, write a decision record with the observed behavior, plausible interpretations, affected cases, owner, chosen future rule, and migration treatment. Do not label the new choice as recovered knowledge. That honesty prevents a later auditor or engineer from treating a fresh policy decision as historical fact.
Absence also limits confidence. Logs may omit rejected records. Database snapshots may show final state without intermediate side effects. Tickets favor failures over successful routine work. Interviews reflect memory and current incentives. State these blind spots beside the conclusions they weaken. A confidence score without an explanation of missing evidence is decoration.
There is a useful stopping rule. Continue investigation while a new piece of evidence could change a consequential implementation or policy decision. Stop when remaining uncertainty has an owner, a containment plan, and no reasonable path to better evidence. Archaeology can consume any budget if nobody defines what decision the excavation supports.
Turn findings into an executable specification
The reconstructed specification should let engineers build and challenge a replacement, not merely admire a diagram. Combine machine readable contracts with short prose for decisions and uncertainty.
For each business capability, record inputs, outputs, state transitions, invariants, error outcomes, timing, permissions, external dependencies, and evidence references. Add examples drawn from sanitized production cases. Put arithmetic rules in executable tests, file layouts in schemas, API behavior in contract cases, and operator decisions in workflow definitions. Prose should explain why an assertion exists and where it may be incomplete.
Organize the specification around business events rather than old modules. A single "post payment" event may cross a screen, a COBOL program, a stored procedure, a nightly extract, and a report. Copying the old folder tree into documentation hides that chain. An event view makes ownership and parity visible across technical boundaries.
Give every assertion one of four dispositions: preserve, intentionally change, retire, or investigate. An intentional change needs an owner and a rollout plan for affected consumers. A retirement needs reachability evidence. An investigation needs a bounded question and a deadline tied to a build decision. This prevents open questions from living forever in comments.
Review with adversarial examples, not a slide presentation. Ask the operator to find a missing recovery path. Ask finance to provide a transaction that crosses a period boundary. Ask the integration owner which malformed records they still send. Run those cases through the old system where safe, add the observations to the ledger, and update the executable cases. People remember exceptions when they can react to a concrete input and output.
Keep provenance close to tests. When a parity assertion fails, the engineer should see whether the expected value came from code analysis, one production trace, a policy document, or an owner decision. The response differs: repair the replacement, question the sample, or escalate a policy conflict. A bare expected number conceals that choice.
A replacement earns trust through measured parity
The final reconstruction succeeds when the new system can process representative recorded work, produce agreed outcomes, expose intentional differences, and operate through failures. A document alone cannot establish that.
Run the old and new implementations against the same sanitized cases. Compare domain outputs, durable state changes, external messages, error classes, and operator evidence. Normalize only values proven irrelevant. Triage every mismatch into a defect in the replacement, an accepted change, nondeterminism that needs control, or a newly discovered ambiguity. Do not weaken an assertion merely to make the dashboard green.
Sequence migration by observable boundaries. A strangled service endpoint is easy to compare if requests and effects can be mirrored safely. A batch chain may need shadow outputs and reconciliation before cutover. A desktop workflow may first move its calculation behind a shared service while retaining the old interface. The architecture can change substantially while parity cases hold business behavior steady.
This is where whole codebase analysis and traffic based verification fit together. CodeHero reads mixed legacy trees in parallel, rewrites them into Go, Rust, TypeScript, and Postgres, then checks behavior with a parity harness against recorded production traffic; it delivers each project in under 30 days. The useful claim is not that automation rediscovers every vanished intention. It is that automation can recover mechanisms at scale and force behavioral claims through repeatable comparison.
Keep the uncertainty ledger after cutover. New evidence will arrive as seasonal events run, forgotten consumers call an endpoint, and operators encounter old exceptions. Attach monitoring to the assumptions with the highest consequence. When an unknown case appears, route it to the owner named in the decision record instead of letting an engineer guess under incident pressure.
You cannot interview an absent author through their source code. You can build something better: a traceable account of what the code permits, what production has proven, what the business now chooses, and what nobody can honestly know. That account is testable, reviewable, and much harder to lose with the next departure.
FAQ
Can source code alone explain all of a legacy system's business rules?
No. Source code shows implemented conditions and calculations, but it cannot prove whether they express current policy, an old workaround, or an accepted defect. Pair code findings with production evidence and an accountable business decision.
What should we collect before analyzing an undocumented system?
Collect source and build material, deployed configuration, schemas, schedules, production observations, runbooks, and authority records such as contracts or approved changes. Record dates, environments, owners, and gaps so evidence from different periods does not get mixed.
How do you identify dead code safely in a legacy application?
Combine static reachability, deployed configuration, runtime counts, scheduler inputs, and policy records. A branch with no recent executions may still handle an annual or emergency event, so absence in logs is not enough to delete it.
Is production traffic safe to use for legacy system tests?
It can be, but only with controlled access, field minimization, consistent redaction, and retention rules. Preserve business meaning needed for replay while avoiding a second uncontrolled store of credentials or personal data.
What is a parity harness?
A parity harness runs the old and new implementations against the same recorded cases and compares agreed business outcomes. It should compare durable effects and error behavior as well as visible responses, while normalizing only proven volatile fields.
How much production traffic is enough for a rewrite?
There is no defensible universal volume. Sample common work, then add business boundaries, rare branches, period events, failure recovery, and cases from incident history. Coverage depends on the variety and consequence of cases, not the raw count.
Should a rewrite preserve known bugs?
Preserve a bug initially when a consumer depends on it and an immediate change would break service. Label it, test it, and replace it through a coordinated policy or interface change rather than silently correcting it during migration.
How do you document knowledge that cannot be recovered?
Mark it as a decidable unknown, describe observed behavior and plausible interpretations, and assign an owner to choose the future rule. Record that choice as a new decision, not as a rediscovered historical fact.
Do operator workarounds count as system behavior?
Yes. If a successful run depends on an operator deleting a marker, editing a file, or judging a report, that action is part of the deployed workflow. The replacement must automate it safely or preserve it as an explicit human task.
When is legacy knowledge reconstruction complete?
Stop when remaining uncertainty has an owner and containment plan, and further evidence is unlikely to change a consequential build or policy decision. Keep the evidence and uncertainty ledgers after cutover because rare events will expose new cases.