How to Read an Unfamiliar Codebase: A Step-by-Step Method

A repeatable, step-by-step method for reading code you did not write, from the first ten minutes to a working mental model you can explain to someone else.

By The OpenVisio Team · · 14 min read

Opening a repository you did not write feels like walking into the middle of a conversation. Hundreds of files, names that mean something to someone, conventions nobody wrote down. The instinct is to start at the top of the file tree and read alphabetically, or to open whatever file looks important and hope it explains itself. Both waste days.

Reading an unfamiliar codebase well is a method, not a talent. You orient before you read, you read with a question instead of a highlighter, you follow one real flow end to end, and you write down what you learn so it compounds. This guide lays out that method step by step, with the commands and habits that make each step fast. It works on a 5,000-line service and a 500,000-line monorepo, though for the second you will also want the techniques in understanding a large codebase fast.

Key takeaways

  • Never read code without a question. "How does a signup become a row in the users table?" is readable; "the auth module" is not.
  • Orient first: README, build files, dependency manifest, folder layout and git history tell you more in 30 minutes than a day of random file reading.
  • Find the entry points (main, routes, CLI commands, workers) and read outward from them, not inward from utilities.
  • Trace one real flow end to end, ideally while running it with a debugger or log statements.
  • Skim aggressively. Most code is plumbing. Spend your attention on data shapes, boundaries and side effects.
  • Write a one-page model as you go and test it by predicting behavior before you check.

Step 1: Orient before you read any source

The first 30 minutes should be spent on everything except the source files. These artifacts are dense with information and cheap to read.

The README and any docs folder. Even a stale README tells you what the project thought it was. Look for setup steps, architecture notes, and a glossary. Note anything that seems out of date; that is useful information too.

The dependency manifest. package.json, go.mod, pyproject.toml, Cargo.toml, Gemfile or pom.xml tells you the frameworks, the database driver, the queue client, the test runner and the HTTP library. Knowing a project uses Express, Prisma and BullMQ tells you where routes, models and background jobs probably live before you open a single file.

The build and run scripts. The scripts block in package.json, a Makefile, Procfile, docker-compose.yml or CI config shows you every process the system runs. A docker-compose.yml with web, worker, postgres and redis is an architecture diagram in disguise.

The folder layout, two levels deep. Do not expand everything. Look at the top two levels and guess what each folder holds.

# Two levels of structure, ignoring noise
tree -L 2 -I 'node_modules|dist|build|.git|vendor'

# Where the code actually is, by line count per folder
find . -name '*.ts' -not -path './node_modules/*' | xargs wc -l | sort -n | tail -20

The git history. History tells you what is alive, what is dead and who knows what.

# Most frequently changed files in the last year: where the action is
git log --since='1 year ago' --name-only --pretty=format: | sort | uniq -c | sort -rn | head -25

# Who has worked on the project, and how much
git shortlog -sn --since='1 year ago'

# The last 30 commits, one line each
git log --oneline -30

Files that change constantly are either the core of the product or a source of pain; either way you will need to understand them. Files nobody has touched in three years are usually safe to ignore at first.

Step 2: Pick a question worth answering

Reading without a question is how you end up four hours deep in a date formatting helper. Before you open source, write down one concrete question tied to real behavior. Good first questions are about a user-visible action:

  • What happens, step by step, when a user submits the signup form?
  • How does an uploaded file get from the browser to storage?
  • What runs when the nightly billing job fires?
  • How does the CLI turn mytool deploy --env prod into API calls?

Bad first questions are about structure in the abstract: "how does the data layer work" or "what does the utils folder do". Those questions have no stopping point. A question about one flow has a natural end: the response goes back, the row is written, the job finishes.

If you have a ticket, the ticket is your question. If you do not, ask a teammate what the single most important user action is and start there. For tips on phrasing these questions so people (and AI tools) can answer them well, see asking better questions about code.

Step 3: Find the entry point for your question

Every flow starts somewhere the outside world touches the code: an HTTP route, a CLI command, a message consumer, a cron schedule, a UI event handler. Find that point first and read outward from it.

Useful searches, depending on the stack:

# HTTP routes in common frameworks
rg -n "app\.(get|post|put|delete)\(|router\.(get|post)\(" --type ts
rg -n "@(Get|Post|Put|Delete)Mapping" --type java
rg -n "path\(|re_path\(" --glob '**/urls.py'

# Go entry points
rg -n "^func main\(" --type go

# Scheduled and background work
rg -n "cron|schedule|@periodic_task|new Worker\(|consume\(" -i

If the UI has a visible string near the action ("Create account"), search for it. The string leads you to the component, the component leads you to the handler, the handler leads you to the API call, and the API path leads you to the route. That chain of literal searches is the fastest way into most web apps. The full catalog of where entry points hide is in finding the entry point of any repository.

Step 4: Trace one flow end to end

Now read, but only along the path your question defines. Start at the entry point and follow calls in the order they execute. At each function, ask three things:

  1. What comes in? The arguments and their shapes. Look up the types or the validation schema.
  2. What goes out? The return value, and where it goes next.
  3. What changes? Database writes, network calls, cache updates, emitted events, files written. Side effects are where the real behavior lives.

Keep a running trail as you go. A plain text file is enough:

POST /api/signup
  -> routes/auth.ts:42 signupHandler
     validates body with SignupSchema (email, password, inviteCode?)
  -> services/users.ts:88 createUser
     hashes password (bcrypt, cost 12)
     INSERT users  <- side effect
     if inviteCode: INSERT team_members
  -> events/emit.ts:15 emit('user.created')
     consumed by workers/welcome-email.ts  <- async, separate process
  <- 201 { id, email }

Line numbers matter. They let you come back, they let you share the trail, and they force you to actually look rather than guess. When you hit a branch you do not care about right now, write "skipped: admin path" and move on. You are tracing one flow, not all of them.

The detailed version of this for web backends, including middleware, ORMs and async hops, is in tracing a request through a web app.

Step 5: Run it and watch

Static reading builds a hypothesis. Running the code tests it. As soon as you can, make the flow execute and observe it.

  • Set a breakpoint at the entry point and step through. In Node, node --inspect-brk with Chrome DevTools or your editor's debugger; in Python, breakpoint(); in Go, Delve (dlv debug).
  • Add temporary log lines at each step of your trail, run the flow, and confirm the order. Delete them afterwards.
  • Run the tests for that area. Tests are executable documentation. A test named it('rejects expired invite codes') tells you a rule exists, and its setup shows you what data the code expects.
# Run only tests related to the area you are reading
npx vitest run src/services/users
pytest tests/services/test_users.py -k signup -vv
go test ./internal/users/... -run TestCreate -v

When behavior surprises you, that is the most valuable moment in the whole exercise. Write down what you expected and what happened. The gap is a fact about the system you would not have found by reading.

Step 6: Skim the plumbing, study the core

Not all code deserves equal attention. In most codebases, a large share of lines is plumbing: serialization, configuration loading, logging, error wrapping, dependency injection setup. You need to know it exists, not how it works line by line.

Spend your attention on:

  • Data shapes. The core types, database schema and API contracts. If you understand the five most important entities and how they relate, you understand most of the business logic's vocabulary. Look at migrations or the schema file early.
  • Boundaries. Where one module calls another, where the app calls an external service, where sync turns into async. Bugs and design decisions cluster at boundaries.
  • Invariants. Validation, permission checks, and anything that throws. These encode the rules the system protects.

Skim by reading function names and signatures, then only opening bodies that matter for your question. Use "go to definition" and "find references" constantly; they are faster and more accurate than text search for anything with a name.

Step 7: Write the model down

Reading without writing evaporates. By the end of your first flow, produce a one-page note that someone else could use. A useful template:

# Signup flow (as of 2026-11-03)

## Entry
POST /api/signup -> routes/auth.ts:42

## Steps
1. Validate body (SignupSchema)
2. createUser: hash, insert users, optional team_members insert
3. Emit user.created -> welcome email worker (separate process)

## Data touched
users, team_members, email_jobs (via queue)

## Surprises
- Invite codes are checked in the handler, not the service
- Welcome email failure does not fail signup

## Open questions
- Why bcrypt cost 12 here but 10 in the admin reset path?

Then test the model: before you look, predict what happens in a related case ("what if the invite code is expired?"), then check. Each correct prediction raises confidence; each wrong one fixes a misconception before it becomes a bug.

Share the note. Put it in the repo's docs, a wiki, or a pull request description. The open questions section is especially valuable; a senior teammate can answer five of them in ten minutes.

Step 8: Widen the circle

Once one flow is solid, pick a second that overlaps the first but goes somewhere new: the login flow after signup, or the admin view of the same data. Each new flow reuses most of what you already know and adds a few new modules. After four or five flows you will have touched the parts of the system that matter most, and the map in your head starts to look like the real architecture.

At this point, it is worth drawing the module-level picture: which folders depend on which, where the database is touched, which processes exist. You can sketch it by hand, or generate it from the code so it reflects what the imports actually say. The tradeoffs are covered in codebase visualization for developers.

Common mistakes

MistakeWhy it hurtsInstead
Reading the file tree top to bottomAlphabetical order has nothing to do with importanceStart from an entry point tied to a question
Reading utilities firstHelpers make no sense without callersRead callers first, open helpers only when needed
Trying to understand everythingNo stopping point, low retentionOne flow at a time, write it down
Trusting namesUserService may not own usersConfirm with "find references" and running the code
Never running the codeHypotheses stay untestedBreakpoints, logs and tests on day one
Keeping notes in your headLost by next weekA one-page note per flow, shared

Where tools help

Everything above works with an editor, git, and rg. Tools can shorten the orientation steps. A generated map of modules and imports replaces an hour of folder spelunking, and a guided walkthrough of the main flows gives you a first trail to verify rather than one to build from scratch. OpenVisio's Tour does this for a GitHub repo: it indexes the code in your browser and walks you through the codebase stop by stop, with the code, a diagram and slides for each stop. When you have a specific question, such as "where is the signup invite code checked", Ask answers it with the relevant code in view, which you can then verify with the steps above.

Tools give you a draft of the map. The method is what turns the draft into understanding: a question, an entry point, one traced flow, a run to check it, and a note that someone else can use.

reading codecodebase onboardingsoftware engineeringmental models

See it on your own repo

Paste a GitHub URL and get the day-one walkthrough — free, in your browser.

Index a repo

Keep reading