Maintaining Code You Didn't Write and Don't Fully Understand: A Field Manual
Let's establish something upfront, before we get into the strategies and the coping mechanisms and the carefully worded advice: you are not alone in this.
Every developer who has worked on a team of more than one person, for more than a year, has maintained code they didn't write. Most of them have shipped changes to systems they understood imperfectly. A significant number of them have done this while being considered the expert on those systems by their colleagues. The gap between "knows enough to be dangerous" and "actually understands what's happening" is where most professional software development lives.
This is not a confession of failure. It is a description of reality. And the first step toward navigating it is to stop pretending the gap doesn't exist.
The Confidence Gradient
Here is a mental model that took most developers too long to articulate: your understanding of any given piece of code exists on a gradient, not a binary. At one end, you have code you wrote last week that you could explain line-by-line. At the other, you have code that might as well be runes carved into a cave wall by a civilization that left no other records.
Most code you maintain professionally sits somewhere in the middle. You understand the shape of it. You know what inputs it accepts and what outputs it produces. You have a rough model of the major components and how they relate to each other. But the specific mechanism by which a given function produces its result? The reason a particular conditional exists? The edge case that a particular early return is protecting against? Those are gaps. And gaps are fine, until you have to touch the code near them.
The imposter syndrome version of this experience is to interpret every gap as evidence that you're a fraud. The more useful interpretation is that gaps are just the parts of the map you haven't explored yet. The territory exists. Your map is incomplete. These are different problems.
The Two Modes of Learning Legacy Code
When you're new to a codebase, you have the luxury of exploration. You can trace execution paths, read tests as documentation, look at git history to understand why things were built the way they were. This is the slow mode. It's thorough. It builds genuine understanding.
When you're under pressure — which is most of the time in production environments — you don't have that luxury. You have a bug to fix or a feature to ship, and you need to understand just enough to do the thing without breaking other things. This is the fast mode. It's targeted. It builds functional understanding that may have significant gaps.
Both modes are legitimate. The mistake is using fast mode exclusively, forever, until the gaps accumulate into a structural problem. The practical approach is to use fast mode when you have to, but to deliberately schedule slow-mode sessions — even brief ones — when the pressure eases. An hour a week of reading code you're not currently changing pays dividends that are hard to measure but impossible to miss.
Reading Code Like a Detective, Not a Student
Legacy code is not a textbook. It will not explain itself to you in a logical sequence. It will present you with conclusions — the current implementation — and leave you to reconstruct the reasoning that produced them.
This is detective work, and the skills are similar. You're looking for evidence, not exposition.
Start with behavior, not structure. Before you read a function, run it. Watch what it does with known inputs. Build an empirical model of its behavior before you try to understand its mechanism. This gives you a framework for interpreting what you read.
Read the tests. Good tests are documentation. Even mediocre tests tell you something about the expected behavior and the edge cases the original author was thinking about. The absence of tests tells you something too — usually that the original author was moving fast and the code is probably more fragile than it looks.
Read the git history. git log -p -- path/to/file is one of the most underused debugging tools in existence. The commit history tells you what changed, when, and why — assuming the commit messages are worth anything, which is not guaranteed, but is more common than cynics suggest. A commit message that says "fix edge case for negative values" is worth a hundred hours of staring at the code trying to figure out why there's a special case for negative values.
Ask the codebase questions by making changes. Not in production — in a test environment, or a branch that goes nowhere. Change a value. Remove a condition. See what breaks. The error messages and test failures are answers.
The Uncomfortable Truth About Shipping Half-Understood Code
Here it is: you will ship code you don't fully understand. You have already done this. Every professional developer has. The question is not whether to do it — you don't always have a choice — but how to do it responsibly.
Responsible half-understood shipping looks like this: you understand the change you're making. You understand the immediate context. You have tested the specific behavior you're modifying. You don't fully understand why the surrounding code is structured the way it is, but you're confident your change doesn't depend on that understanding.
Irresponsible half-understood shipping looks like: you made a change that seemed right, the tests pass, you're not sure what the tests are actually testing, and you're hoping for the best.
The difference is the scope of your ignorance. Targeted gaps in understanding are manageable. Pervasive ignorance is a liability.
Document what you don't know. This is counterintuitive but important. A comment that says "I'm not certain why this boundary condition is handled this way — see ticket #4821 for context" is more valuable than silence. It tells the next person that this is a known unknown, that there's context to look for, and that the code deserves scrutiny.
The Real Imposter's Checklist
If you're maintaining systems you didn't build and feeling like you're one wrong question away from being exposed, here is what you should actually check:
- Are you making changes that are proportionate to your understanding? ✓
- Are you asking for help when you encounter gaps that matter? ✓
- Are you documenting what you learn as you learn it? ✓
- Are you testing your changes against the behaviors you're trying to preserve? ✓
- Are you honest with your team about what you know versus what you're inferring? ✓
If yes to most of these: you are not an imposter. You are a developer doing the actual job of software development, which has always involved navigating uncertainty with incomplete information and shipping things anyway.
The people who built the systems you're maintaining felt this way too. Some of them still do. The codebase carries their uncertainty in every TODO comment, every overly defensive null check, every function named processData2.
You're in good company. Keep shipping.