How to Understand a Large Codebase Fast (Days, Not Months)

A practical five-day plan for getting a working mental model of a very large repository, by reading structure and history first and source code last.

By The OpenVisio Team · · 13 min read

A 500,000-line repository cannot be read. At a generous 200 lines an hour of careful reading, that is 2,500 hours, more than a working year, and by the time you finished the first half would have changed. Yet engineers join large codebases all the time and become productive in a couple of weeks. They do it by not reading most of the code.

Understanding a large codebase fast means building a working mental model: knowing what the major parts are, how they connect, where the important behavior lives, and where to look when you need detail. That model can be built in days if you lean on structure, history and a handful of carefully chosen flows. This post lays out a five-day plan. For the per-file reading technique that sits underneath it, see how to read an unfamiliar codebase.

Key takeaways

  • Your goal is a map, not coverage. You need to know where things are and how they connect, not what every line does.
  • Read the shape before the content: folder structure, build targets, package boundaries and the dependency graph.
  • Use git history to find the 5 percent of files that matter: high churn plus high centrality marks the core.
  • Trace three to five critical user flows end to end. They cross most of the important modules.
  • Ignore whole regions on purpose. Generated code, vendored code, old migrations and dead features can wait.
  • Write the model down as a one-page map and have someone senior correct it. Corrections are the fastest learning you will get.

What "understanding" means at this scale

In a small project, understanding means you could rewrite any part of it. In a large one, that is impossible and unnecessary. A working mental model has four layers:

  1. Inventory. The major subsystems and what each is for, in one sentence each.
  2. Topology. Which subsystems depend on which, which processes exist, where data is stored, and what crosses the network.
  3. Hot core. The 20 to 50 files where most important behavior and most change happen.
  4. Index. For any question, you know which part of the repo to search first and who to ask.

If you have those four layers, you can take a ticket anywhere in the codebase, find the right place in minutes, and read deeply only there. That is the target for the end of week one.

Day 1: Inventory from structure

Spend day one without reading function bodies. You are building the inventory.

Count and classify. Find out what is actually in the repo. Tools like cloc or tokei break it down by language and folder:

# Lines of code by language, excluding vendored and generated folders
tokei --exclude node_modules --exclude vendor --exclude dist

# Lines per top-level folder
for d in */; do echo "$(find "$d" -type f \( -name '*.go' -o -name '*.ts' -o -name '*.py' \) | xargs cat 2>/dev/null | wc -l) $d"; done | sort -rn

You will often find that the "500k-line" repo is 150k lines of real application code, 100k of tests, and the rest generated clients, fixtures, vendored libraries and migrations. That is good news: the job just got smaller.

List the deployables. Find every process the repo produces. Look at Dockerfiles, CI deploy jobs, Kubernetes manifests, Procfile, cmd/ directories in Go, workspace packages with a start script, or [[bin]] targets in Rust. Each deployable is a separate entry point with its own lifecycle.

List the packages or modules. In a monorepo, the workspace config is the table of contents: pnpm-workspace.yaml, the workspaces field in package.json, go.work, a Cargo workspace, Bazel BUILD files, or Gradle settings.gradle. Write one line per package describing what you think it does, based on its name, README and dependencies.

By the end of day one you should have a list like this:

apps/web          Next.js customer app
apps/admin        internal admin UI
services/api      main HTTP API (Go), talks to Postgres + Redis
services/billing  billing worker, consumes Kafka topic invoices.*
packages/sdk      generated API client, do not edit
packages/ui       shared React components
libs/auth         token issuing and verification, used by api and billing

Some lines will be wrong. That is fine; you are writing a hypothesis.

Day 2: Topology from the dependency graph

Day two answers "what depends on what". Imports are the ground truth for this, more reliable than any diagram in the wiki.

For JavaScript and TypeScript, madge or dependency-cruiser can produce a module graph. For Go, go list shows package imports. For Python, pydeps or import-linter contracts. For Java, jdeps.

# TypeScript: graph of imports, as an image
npx madge --extensions ts,tsx --image graph.svg src/

# Go: which internal packages each package imports
go list -f '{{.ImportPath}}: {{join .Imports " "}}' ./... | grep -v vendor

# Python
pydeps mypackage --max-bacon 2 --cluster

On a large repo, the file-level graph is unreadable. Collapse it to the package or folder level. You are looking for a few shapes:

  • Hubs: modules almost everything imports. Usually core types, config, auth or a database layer. Learn these early; you will see them in every flow.
  • Leaves: modules nothing else imports. Often entry points, scripts, or dead code.
  • Layers: does the graph flow in one direction (handlers to services to repositories), or is it tangled?
  • Cycles: packages that import each other. They mark places where boundaries are blurry and changes ripple.

Add the cross-process edges too: which service calls which over HTTP or gRPC, which publishes to which queue. These do not show up in imports, so search for client construction, topic names and base URLs. The dependency graph explainer goes deeper on reading these shapes.

Day 3: Find the hot core with history

Git history tells you where the important code is. Two signals combined are especially strong: churn (how often a file changes) and centrality (how many other files depend on it).

# Churn: most-changed files over the last 12 months
git log --since='12 months ago' --name-only --pretty=format: \
  | grep -v '^$' | sort | uniq -c | sort -rn | head -40

# Files changed together often (coupling): look at big recent commits
git log --since='6 months ago' --pretty=format:'%h %s' --shortstat | head -60

# Who knows each hot file
git log --since='12 months ago' --pretty=format:'%an' -- services/api/orders/handler.go | sort | uniq -c | sort -rn

Take the top 40 churned files and cross-reference them with the hubs from day two. Files that are both frequently changed and widely depended on are the hot core. In most large repos this is a surprisingly short list, a few dozen files, and it is where the product's real logic lives: the order state machine, the permission checks, the pricing rules, the main API router.

Read these files. Not every line, but enough to know what each one owns. This is the first day you spend significant time in source, and it is targeted at the code that matters most. The method for combining churn and complexity is covered in more depth in code hotspots and churn analysis.

Day 4: Trace three to five critical flows

Now pick the flows that matter most to the business. Ask a product manager or a senior engineer: "If these five things broke, we would be down." Typical answers are sign in, the core create action (place an order, create a document, send a message), payment, search, and one important background job.

Trace each one end to end, from the entry point to storage and back, writing down file and line at each step. Because these flows are critical, they cross most of the hot core you found on day three, and they will confirm or correct your topology from day two. A request that you thought went straight to the database may turn out to pass through a cache, a feature flag service and a permissions library.

Run each flow locally if you can, with a debugger or verbose logging. In a large system you may not be able to run everything; in that case, read the integration tests for the flow, which often set up exactly the pieces you need. The detailed technique is in tracing a request through a web app.

Day 5: Write the map and get it corrected

Spend the last day writing down what you know, then putting it in front of someone who knows more. A one-page map works well:

# Codebase map (draft, 2026-11-03)

## Deployables
web (Next.js), api (Go), billing-worker (Go), admin (React)

## Core flow: place order
web checkout -> api POST /orders -> orders.Service.Place
  -> inventory.Reserve (sync, same process)
  -> payments client -> Stripe (external)
  -> publish orders.placed (Kafka) -> billing-worker, email-worker

## Hot core (top 10)
services/api/orders/service.go     order state machine
libs/auth/verify.go                every request passes through here
...

## Ignored for now
packages/sdk (generated), legacy/reports (being removed), migrations/ before 2023

## Questions
- Why does billing read orders table directly instead of via api?

Then book 30 minutes with a senior engineer and ask them to mark it up. They will catch the wrong arrows, the subsystems you missed, and the history ("billing reads the table directly because the API was too slow in 2022, we want to undo that"). This conversation is worth more than any other half hour of the week, and it only works because you arrive with a concrete draft instead of "can you explain the architecture?"

What to ignore, on purpose

Speed comes as much from what you skip as from what you read. Safe to skip in week one:

RegionWhyWhen to look
Generated code (clients, protobuf, ORM output)Derived from a source you can read insteadWhen debugging serialization
Vendored dependenciesNot your codeWhen a bug is in a dependency
Old migrationsThe current schema matters, not its historyWhen you need to know why a column exists
Feature-flagged-off or deprecated featuresMay be deleted soonIf your ticket touches them
Build tooling internalsUsually stable and owned by one personWhen the build breaks
Test fixtures and snapshotsData, not logicWhen a test fails

Write the ignore list into your map so you remember the decision was deliberate.

Habits that keep the model current

A large codebase changes under you. A few habits keep your model honest after week one:

  • Read merged PRs in areas near your work. Ten minutes a day of reading other people's diffs teaches you more of the codebase than any other passive activity.
  • Update the map when you are surprised. Each time a flow does not go where you expected, fix the map. If the map is shared, the next new person benefits.
  • Use impact analysis before changing hubs. Before editing a widely imported module, check who depends on it. The approach is in impact analysis before refactoring.

Using generated maps to skip the slow parts

Days one and two are mostly mechanical: inventory, imports, grouping files into modules. That is exactly the work a tool can do from the code itself. OpenVisio's Explore space indexes a GitHub repo in your browser and shows the architecture as a module graph alongside the file tree and source, so you can start day two with the topology already drawn and spend your time checking it instead of building it. The Tour adds a guided walkthrough of the main parts, which makes a good first draft of the day-five map.

Generated or hand-made, the map is only a starting point. The understanding comes from tracing real flows and getting your draft corrected by someone who has lived in the code. Do that, and a repo that looked unreadable on Monday is navigable by Friday.

large codebasescodebase onboardingarchitecturegit historyhotspots

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