DEV Community

Ali Raza
Ali Raza

Posted on

The Developer Skill Nobody Teaches: Reading Code You Didn't Write

The fastest developers are not always the ones who write code fastest. They are often the ones who can understand an unfamiliar codebase without getting lost.

There is a moment almost every developer eventually experiences.

You open a repository you did not build.

The README looks incomplete.

There are folders you have never seen before.

A function calls another function, which calls a service, which talks to a repository, which eventually touches a database.

You search for the feature you need to change.

You find the file.

Then you realize something uncomfortable:

You have no idea why the code works this way.

So you start reading.

Five minutes become thirty.

Thirty minutes become two hours.

And you still feel like you are missing something.

This is not a sign that you are a bad developer.

It is a sign that reading unfamiliar code is a different skill from writing code.

Software engineering research has treated program comprehension as a major part of software maintenance for decades. Developers frequently have to understand systems they did not originally create, often with incomplete documentation.

Yet most developers spend years learning how to write code and surprisingly little time learning how to read code systematically.

That skill deserves more attention.

Writing Code and Reading Code Are Different Problems

When you write code, you already know the intention.

You know what you are trying to build.

You know why a function exists.

You know what a variable means.

You know which assumptions you made.

When you read someone else's code, none of that context is guaranteed.

You are working backward.

Instead of:

Problem → Design → Code

you are doing:

Code → Behavior → Design → Original Intent

That is closer to reverse engineering.

You are reconstructing a mental model from evidence.

And that is why simply knowing a programming language is not enough.

You can be excellent at JavaScript and still struggle to understand a large JavaScript application.

You can know Python deeply and still spend hours navigating an unfamiliar Django codebase.

The language is only one layer.

The real challenge is understanding how the pieces work together.

Why Unfamiliar Code Feels So Difficult

When developers enter an unfamiliar repository, several problems appear at once.

You may not know:

Where the application starts
Where business logic lives
How data moves through the system
Which files are important
Which functions are legacy code
Which abstractions are intentional
Which dependencies are external
Where errors are handled
What assumptions the system makes
Which parts are safe to change

This creates cognitive overload.

Research into program comprehension has found that developers working on unfamiliar systems search for relevant code, follow dependencies, and gather information while trying to understand the system. That exploration itself can become expensive and inefficient.

The mistake is thinking:

"I need to read the code."

You usually don't.

You need to build just enough understanding to answer the question in front of you.

That is a very different approach.

Don't Read the Repository. Build a Map.

Imagine opening a new city for the first time.

You would not walk through every street before deciding where to go.

You would first look at a map.

Codebases deserve the same treatment.

Before reading individual functions, identify the major areas of the system.

For example:

src/
├── controllers/
├── services/
├── repositories/
├── models/
├── middleware/
├── utils/
└── config/

You do not need to understand every file.

First ask:

What role does each area play?

Maybe:

Controller

Service

Repository

Database

Now the repository already feels smaller.

You have created a mental map.

That map becomes the foundation for everything you read next.

Start With the User's Journey

One of the fastest ways to understand an application is to follow a real user action.

Suppose the task is:

"Fix the issue where users cannot update their profile."

Do not randomly open files.

Start from the behavior.

Ask:

Where does the request enter?

Which route handles it?

Which controller receives it?

Which service performs the operation?

Which repository accesses the database?

What response comes back?

For a web application, the flow might look like:

Browser

HTTP Request

Route

Controller

Service

Repository

Database

Repository

Service

Controller

HTTP Response

You are not reading everything.

You are following one vertical slice through the system.

This approach is especially useful when debugging or implementing a feature because it connects code to actual behavior.

Search Is Not Just a Tool. It Is a Thinking Skill.

Experienced developers do not necessarily read more code.

They often search better.

Suppose you need to understand how authentication works.

Search for:

login
authenticate
session
token
jwt
authorization

Then look at the relationships between the results.

For example:

login()

authenticateUser()

verifyPassword()

generateToken()

Now search for where the token is consumed:

verifyToken()
requireAuth()
Authorization

You are gradually reconstructing the authentication flow.

A 2025 study on developer code comprehension also highlights that understanding code involves more than purely technical knowledge and can involve cognitive and non-technical factors.

That is important because code comprehension is not simply:

"Can you understand this function?"

It is:

"Can you construct an accurate mental model of how this system behaves?"

Follow Dependencies, Not Just Files

A common mistake is reading files from top to bottom.

That can work for small programs.

It becomes inefficient in large systems.

Instead, follow relationships.

If you find:

await userService.updateProfile(userId, data);

do not stop there.

Jump into:

updateProfile()

Then ask:

What does it call?
What does it return?
What assumptions does it make?
What can fail?

Maybe you discover:

async function updateProfile(userId, data) {
const user = await userRepository.findById(userId);

validateProfile(data);

return userRepository.update(userId, data);
}

Now you have another dependency:

userService

userRepository

Continue only as far as necessary.

This is more efficient than reading every unrelated utility in the repository.

Read Names Before Reading Logic

Names are clues.

A function called:

calculateInvoiceTotal()

already tells you something.

So does:

validatePayment()

or:

createSubscription()

Before reading implementation details, ask what the names suggest.

Then verify whether the implementation matches your expectation.

This is also why naming matters so much in maintainable software.

Google's code review guidance specifically emphasizes clear naming, understandable code, appropriate documentation, testing, and keeping complexity under control.

Good names reduce the amount of mental reconstruction another developer has to perform.

Comments Should Explain the "Why"

A common mistake when reading code is assuming every confusing section needs more comments.

Sometimes it does.

But comments should not simply translate code into English.

Bad:

// Increment count by 1
count++;

That adds almost no information.

More useful:

// Retry only idempotent requests because POST may create
// duplicate records when repeated.

The second comment explains a decision that may not be obvious from the code itself.

Google's code review guidance similarly recommends comments that explain why rather than simply describing what the code already says.

When reading unfamiliar code, pay special attention to these comments.

They often reveal decisions that the code alone cannot explain.

Don't Assume Confusing Code Is Bad Code

This is an important rule.

You encounter a strange abstraction.

Your first thought might be:

"Who wrote this?"

Do not immediately conclude that the code is wrong.

There may be a reason.

Maybe the abstraction exists because:

The system supports multiple providers
A legacy API must be isolated
Testing requires dependency injection
Several products share the same service
A database limitation shaped the design
A performance problem required caching

Martin Fowler describes technical debt as internal quality problems that make future changes harder, but a confusing structure does not automatically prove that the code is defective.

Similarly, Fowler's discussion of code smells points out that a smell is an indicator that deserves investigation, not automatically proof of a problem.

So when you see something strange, ask:

"What problem might this design be solving?"

That question is much more useful than:

"Why didn't they just do it my way?"

Build a Mental Model Before You Refactor

One of the most dangerous things you can do in an unfamiliar codebase is refactor too early.

You see duplication.

You see a long function.

You see an unusual architecture.

You want to clean it up.

Stop.

First understand the behavior.

Ask:

What does this code do?
Who depends on it?
What assumptions exist?
What tests protect it?
What edge cases matter?

Only then consider changing it.

Google's code review guidance emphasizes improving overall code health while balancing forward progress and avoiding unnecessary perfectionism.

The principle is simple:

Understand first. Change second.

Use Tests as Documentation

When documentation is missing, tests can reveal expected behavior.

Suppose you find:

calculateDiscount(order)

The implementation may not tell you all the business rules.

But the tests might:

it("does not apply a discount to expired coupons");

it("applies 10% discount to premium users");

it("does not allow discount below minimum order value");

Now you understand the domain better.

Tests tell you what the system considers important enough to protect.

When reading unfamiliar code, search for:

*.test.js
*.spec.js

or whatever testing convention the project uses.

Then compare:

Implementation
+
Tests
+
Callers
+
Documentation

Together, they provide a much stronger picture than any single source.

Run the Code

There is a limit to how much you can understand by staring at source files.

Eventually, run the application.

Add a breakpoint.

Inspect a variable.

Watch an HTTP request.

Look at logs.

Send a request manually.

Run a test.

Observe the database query.

Static code tells you what could happen.

Runtime behavior tells you what is actually happening.

This distinction becomes especially important in systems with:

Dependency injection
Middleware
Event-driven architecture
Async operations
Configuration-based behavior
Feature flags
Dynamic imports
External services

The running application is another source of documentation.

Ask Questions Like a Senior Developer

If you are new to a codebase, asking another developer is not failure.

It can be one of the fastest ways to understand the system.

But the quality of the question matters.

Instead of:

"How does authentication work?"

Try:

"I traced login from the route to AuthService.login(). It generates the token there, but I cannot find where the token is validated for protected requests. Is that handled by the middleware?"

That question shows:

You investigated.
You have a hypothesis.
You know exactly where you are stuck.

GitHub's engineering guidance similarly recommends asking questions during code review, particularly around assumptions, data shape, resource usage, and behavior in unfamiliar codebases.

Good questions accelerate learning.

A Practical 30-Minute Workflow

Next time you inherit an unfamiliar repository, try this workflow.

Minutes 0 to 5: Understand the Project

Read:

README
package.json / requirements.txt
configuration files
entry points

Find out:

What does this application do?
What stack does it use?
How is it started?
Where does execution begin?
Minutes 5 to 10: Map the Architecture

Identify:

Routes
Controllers
Services
Database
Models
Tests
External APIs

Do not read everything.

Just locate them.

Minutes 10 to 20: Follow One Feature

Pick the feature related to your task.

Trace:

Input

Entry point

Business logic

Data access

Output
Minutes 20 to 25: Read Tests

Look for expected behavior and edge cases.

Minutes 25 to 30: Run Something
https://goodoff.co/
Run:

a test
the application
an API request
or a small debugging session

At the end of 30 minutes, you may not understand the whole repository.

You should not expect to.

But you should have a map.

And a map is enough to start moving.

What About AI?

AI can make unfamiliar code easier to understand.

But there is a trap.

You can paste an entire repository into an AI tool and ask:

"Explain this project."

You may receive an impressive summary.

But reading the explanation is not the same as developing your own mental model.

A better approach is to use AI as a navigation assistant.

Ask questions such as:

What does this function appear to be responsible for?

What are the dependencies of this module?

Explain this error path.

What assumptions does this function make?

What edge cases should I investigate?

Help me trace how this request reaches the database.

Then verify the answers against the actual code.

AI can accelerate exploration.

It should not replace verification.

This is particularly important because generated explanations can sound confident even when they misunderstand project-specific behavior.

Your repository remains the source of truth.

The Real Goal: Reduce the Unknown

When you first open an unfamiliar codebase, almost everything is unknown.

Your job is not to eliminate all uncertainty immediately.

Your job is to reduce it systematically.

Start with:

Unknown

Architecture

Feature

Dependencies

Runtime behavior

Business rules

Safe change

Each step reduces uncertainty.

This is what good developers do naturally.

They do not magically understand large codebases.

They know how to investigate them.

The Developers Who Read Well Become Better Engineers

Writing code is visible.

Reading code is mostly invisible.

You see the developer who writes a feature in two hours.

You do not see the three hours they spent understanding the existing architecture before writing it.

You see the final pull request.

You do not see the investigation behind it.

That investigation is engineering work.

Program comprehension has long been recognized as a central part of software maintenance, and research continues to examine how developers understand unfamiliar systems and which technical and cognitive factors affect that ability.

This is why reading code deserves to be treated as a real engineering skill.

Because in professional software development, you will spend a lot of time working with code you did not write.

Sometimes you will inherit it.

Sometimes another team will own it.

Sometimes the original developer will have left.

Sometimes the documentation will be outdated.

Sometimes the code will be older than your career.

And sometimes the only reliable explanation will be the code itself.

Top comments (0)