What to do with the code nobody understands

Every system we are handed has one. The module everyone avoids. The file where estimates triple the moment it appears in a ticket. The part where the answer to "how does this work" is a pause, then "you should ask whoever wrote it", then the news that they left in March.
Two instincts show up immediately, and both are wrong for the same reason.
The first is to rewrite it. The second is to leave it alone forever. Neither works, because you do not yet know which of its behaviour is deliberate and which is accidental, and that single piece of missing information is what makes both plans dangerous.
Why the rewrite fails
A rewrite reproduces what you can see. The trouble is that the valuable parts of an old module are frequently the parts you cannot see: the condition added after an incident nobody documented, the special case for one large customer, the ordering that looks arbitrary and exists because a downstream system needs it that way.
Read the code and those look unimportant. Rewrite from a clean understanding and they disappear, along with a guarantee somebody was relying on. Then it comes back one support ticket at a time, over several months, and each one arrives without the context that would let you recognise it as the thing you deleted.
We have written about the broader decision in when to rewrite vs refactor legacy code. The short version for this situation: a rewrite is a reasonable choice once you understand the module, and it is the most expensive possible way to try to understand it.
Why ignoring it fails
The cost of avoidance does not stay the same. It keeps growing.
Each feature built around the module adds a layer that exists only because nobody would edit the original. Logic gets duplicated nearby because duplicating felt safer than modifying. The interface becomes fixed, since changing it would require working inside the module. After two years the module is both essential and impossible to change, which is the worst combination available, and the cost is now spread across the whole system where nobody can see it as one line item.
The reliable signal is the estimate. When work touching that area is quoted at three times work of the same size elsewhere, avoidance has stopped being free.
What actually works
Three steps. None of them is clever, and the order matters more than the technique.
1. Map the boundary, not the internals
Do not start by reading the implementation. Start by finding the boundaries.
What calls into it, and from where. What it calls out to. What it reads and what it writes. What the caller expects back, including the failure cases. What it does that is not a return value: writes to a queue, sends an email, updates a cache, records something for a report.
That last category is where the surprises are, and it is the one people forget to look for. A function whose only visible output is a boolean can still be the reason a nightly report is correct.
The boundary is a much smaller object than the implementation, and you can usually finish it in an afternoon. It also tells you the thing you most need: how much damage a change in here could do.
2. Record what it does now
Take real inputs, including the messy ones, run them through, and assert the outputs you observe.
If there is a bug in there, you capture the bug as expected behaviour. That is correct at this stage, and it is the part that feels wrong to people the first time. You are not certifying that the module is right. You are building a check that will tell you when something changed that you did not mean to change.
Two things come out of this. You get permission to edit, because now a mistake is visible. And building the fixtures teaches you the module faster than reading it, because you have to understand the inputs well enough to construct them. The method in detail is in adding evals to an existing codebase.
One caution: the checks must be able to fail. A suite generated by reading the implementation asserts that the code does what it does, passes automatically because of how it was made, and gives you nothing except confidence. Break something deliberately and confirm it is caught. The full list of these failures is in when evals give false confidence.
3. Change it in provable steps
Now edit, in increments small enough that you can say in one sentence what should stay identical, and then confirm it did.
Rename one thing. Extract one function. Split one branch. Each step is boring on its own and the sequence gets you somewhere a single large refactor would not, because you are never in the position of holding twenty simultaneous changes and trying to work out which of them caused the failure.
This is slower per change and faster overall. Most of the time lost in a big refactor is spent searching through your own changes to find the one that broke something.
The part people get wrong about difficulty
Most frightening modules are unprotected: nothing tells you when a change breaks them.
The fear comes from having no way to know whether a change broke something, and that is a property of the surrounding checks rather than of the code itself. It is why step two so often makes a module ordinary to change again without altering a single line inside it. The code did not change. What changed is that a mistake now shows up in eight seconds instead of five weeks.
Worth checking before you assume the work is large: sometimes the module is fine and the fear is inherited from an incident three years ago that nobody has revisited.
What generated code changes
Hand-written legacy code shows its own problems. The inconsistent file is where two people disagreed, the function with four styles in it is where the deadline pressure showed, the commented-out block has a history someone could explain.
Generated code removes those cues. It is uniform, plausibly named, and tidy across both the parts that were carefully specified and the parts that were invented. Consistency stops carrying information.
So you cannot find the dangerous areas by looking for mess. You find them by asking what the system must guarantee, then tracing those guarantees specifically: anything moving money, anything deciding who can see what, anything that deletes, anything a customer makes a decision from. Veracode's Spring 2026 testing across more than 150 models found generated code running correctly over 95 percent of the time while passing security checks in roughly 55 percent, with cross-site scripting passing at 15 percent and log injection at 13 percent. Those kinds of defects are invisible at reading speed, which is precisely why looking for visible mess does not find them.
When leaving it alone is the right answer
It genuinely can be. Judge it on two measures: how often the module changes, and how bad a failure would be.
Rarely changed, where a failure would break little, means leave it, and say so out loud so the decision is recorded rather than merely inherited. Affected by most new work, or handling money, permissions, or personal data, means make it safe to edit even if you have no plans to edit it.
The failure mode is letting avoidance pass as a decision. Writing down "we are choosing not to touch this, and here is why" costs five minutes and stops the question being argued again every quarter.
The person who is the only one who knows
If someone still understands it, that is a resource that will not be available forever.
Have them explain it step by step while someone else writes down what it must guarantee, not how it works. Then turn those guarantees into checks. Documentation goes out of date and cannot be verified. A check keeps working after the person leaves, and it fails clearly when the assumption stops being true.
Also write down what is still unclear afterwards, honestly. That section is the most valuable one and the one people most want to omit, because a confident diagram with three guesses in it gets trusted, and the guesses quietly become facts. Related reading is in how to take over a codebase you did not write.
Thanks to the engineers who showed us the modules their teams avoid and were honest about how long the avoidance had been going on. In nearly every case the code was ordinary and the missing thing was any way to tell whether a change had broken it.
Code nobody understands is rarely too complicated to learn. It is usually just unprotected, and protection is the cheaper half of the job.
Related guide: Maintaining custom software.
Sources
- Veracode, Spring 2026 GenAI Code Security update: across more than 150 models, over 95 percent syntactic correctness against roughly 55 percent security pass rate, with cross-site scripting at 15 percent and log injection at 13 percent.


