Engineering Lessons

The Art of Reading Code You Didn't Write

Jul 6, 2026·7 min read
"The comment that said 'do not remove this, it will break everything' was not in the code. It was in a commit message from 2019 that I found by accident."

The natural thing to do when you join a new codebase is to start reading the files. This is the wrong place to start, and it took me several years and several codebases to understand why.

Reading files tells you what code exists. It does not tell you why the code is shaped the way it is, which parts of it are relied upon in ways that aren't obvious from the code itself, which apparent redundancies are load-bearing, or what constraints the original authors were working within that no longer exist but whose effects are still present in the structure. A file is the end result of a series of decisions made over time by people with context you don't have. Reading the file without the context tells you the decision but not the reasoning.

Here is how I approach a new codebase now, after learning this slowly.

Start With the Tests

Tests, when they exist and are reasonably maintained, are a form of documentation that is executable. They describe what the code is expected to do, under what conditions, with what inputs, producing what outputs. They often encode edge cases, negative numbers, empty strings, null values, concurrent access, that the business requirements document never mentioned but that clearly occurred to someone at some point because the test is there.

More specifically, start with the failing tests if any exist, then the integration tests, then the unit tests. Failing tests tell you what the codebase knows it doesn't handle correctly. Integration tests show you how components interact at the boundaries. Unit tests show you the atomic behavior of individual pieces. Reading tests in that order builds a mental model of the system's intended behavior before you read the code that implements it, which means when you read the implementation, you have a frame for understanding what it's trying to do.

In codebases without tests, or with tests that are clearly out of date (tests that test things that no longer exist, or that pass against code they shouldn't be able to pass against), the absence is itself information. It tells you that the behavior of the system is not formally documented and that changes carry unknown risk. This affects how cautiously you make your first changes.

Read the Git Log Before the Code

The commit history is the narrative of how the codebase arrived at its current state. It tells you which parts of the codebase have been changed frequently (suggesting either active development or recurring problems), which haven't been touched in years (suggesting either stability or orphaning), and, in teams with descriptive commit messages, why specific changes were made.

The most useful thing I've found in commit history is not the code changes themselves but the context around them. A commit that adds ten lines of defensive null checking around a specific API call, with a message like "handle null response from legacy pricing service after migration incident," tells me something important: that API call has a known history of returning null under certain conditions that a migration created, and the null check is not paranoid defensive programming, it's a scar from a real incident. If I later decide to refactor the code around that API call and remove the null check because it looks unnecessary, I've made a mistake that the commit history could have prevented.

I once spent a day investigating a mysterious block of code in a billing system that appeared to duplicate a calculation that was performed correctly in a different part of the same function. The code ran conditionally, under circumstances that my testing didn't trigger. It appeared to be dead code that had been superseded. The git blame showed it had been added in a single commit three years earlier with the message: "hotfix: handle negative quantity items in bulk orders, this case breaks the main path." It was not dead code. It was a special case handler for a scenario that I had never thought to test and had never observed in normal usage. Removing it would have been a breaking change with consequences that would only appear when a specific customer placed a specific type of order.

Find the Entry Points

Large codebases can be approached at any point, but the high-leverage entry points are usually a small number of them: the API endpoints that external callers use, the message handlers that process events from a queue, the scheduled jobs that run on a timer, the database schema. These are where the outside world meets the system. Understanding what happens at each entry point, what data comes in, what happens to it, what goes out, gives you a structural picture of the system that file browsing cannot.

In a web service, I start with the router or controller layer. Every route is a contract with the outside world. For each route: what parameters does it accept, what state does it read, what state does it change, what does it return, what can go wrong. Going through each route in this way, even quickly, builds an accurate model of the system's surface area that's difficult to get any other way.

In an event-driven system, the equivalent is the message handlers: what topics does this service subscribe to, what does it do when each message arrives, what topics does it publish to. The data flow, what messages come in and what messages go out in response, is often more illuminating than the implementation of any individual handler.

Look for What's Missing

Well-structured code tells you what the system does. The gaps, places where you'd expect something to exist but it doesn't, often tell you about the constraints the original authors were working under, or about assumptions so obvious to the domain that nobody thought to make them explicit.

A billing system I worked on had no validation on the currency field of transaction records. The field accepted any string. In the five years the system had been running, the currency had always been set by automated processes from a controlled list, so the absence of validation had never caused a problem. But the absence was also not documented anywhere. A new integration that set the currency field from an external source would fail silently in ways that were very hard to diagnose, because the system's behavior when encountering an unrecognized currency code was not defined anywhere, it was simply never encountered.

Looking at what's missing also surfaces implicit assumptions that the codebase treats as universal. A codebase that assumes all amounts are positive integers in pence, and has never needed to handle negative amounts or fractional units or foreign currencies, will have those assumptions scattered invisibly through the code, not as explicit constraints, but as the shape of the data model, the arithmetic, the display logic. Discovering those assumptions early prevents the design mistakes that come from not knowing they exist.

Ask About the Bits That Don't Make Sense

Every codebase I have worked on contains at least one piece of code that appears to be unnecessarily complex, or redundant, or wrong, but has actually survived because removing or simplifying it would break something that isn't obvious from reading the code in isolation.

The correct response to code that doesn't make sense is not to simplify it. The correct response is to ask someone who has been around long enough to know why it's there. "Can you explain this to me?" is one of the most efficient uses of time in a new codebase, and the answer is often "oh, that handles a legacy case from when we were on version X of the API" or "that's there because of a regulatory requirement we had to add in 2021" or "actually, you're right, that can probably come out, I'd want to test it carefully though."

In the specific case where nobody knows why something is there, that information is also useful. Code that nobody understands the purpose of, that appears to be functioning, and that nobody is confident enough to remove, is technical debt in a very specific form: it prevents change because the cost of getting it wrong is unknown. Understanding that it exists, and flagging it explicitly rather than treating it as normal background complexity, is the first step toward eventually resolving it.

The Frame for Everything

The underlying principle of reading code you didn't write is that you are trying to reconstruct the reasoning that produced the code, not just to understand what the code does. The what is in the code. The why is in the tests, the commits, the architecture decisions, the conversations with people who were there, and the gaps where reasoning that was too obvious to document has left traces in the structure.

The developers who move fastest in a new codebase are usually not the ones who read the most code fastest. They are the ones who spend time understanding the context before writing any code themselves, who ask questions that seem too basic to ask, and who treat every confusing thing they encounter as a signal worth investigating rather than noise to push through.

The confusing thing is often the most important thing in the file. The code around it is usually straightforward.

Tags

#code-reading#legacy-code#onboarding#engineering-skills#codebase