4 min read
On this page

Navigating Unfamiliar Codebases

You join a new team. The repository has 500 files, 12 directories, a build system you have never used, and zero useful documentation. Everyone else knows where things are because they built it. You need to become productive in this codebase as fast as possible. This is not a one-time problem — engineers change teams, inherit legacy systems, review code in adjacent services, and evaluate open-source libraries constantly. Being good at navigating unfamiliar code is a permanent competitive advantage.

Where to Start

The instinct is to start reading from the top of the directory tree. That is wrong. Directory structure tells you how someone organized files, not how the system works. Start with entry points.

Find the Entry Points

Every program has a place where execution begins. Find that first:

Web server:      main.py, app.py, server.js, index.ts, main.go
CLI tool:        main(), __main__.py, bin/ directory
Library:         the exported public API (index.ts, __init__.py, lib.rs)
Mobile app:      AppDelegate, MainActivity, App.tsx
Worker/daemon:   the file that starts the event loop or polling
Serverless:      handler functions (handler.py, index.handler)

Read the entry point file first. It tells you:

  • What frameworks and libraries are used
  • How the application is initialized
  • What the top-level structure looks like
  • Where configuration is loaded from
Typical entry point reading:
  1. Open main entry file
  2. Note the imports (these are the key dependencies)
  3. Follow the initialization order (what gets set up first?)
  4. Find where routes/commands/handlers are registered
  5. Pick ONE route or handler and follow it end-to-end

Follow One Request End-to-End

After finding the entry point, pick a single request path and trace it from start to finish. For a web application:

  HTTP request arrives
    -> routing layer (which handler?)
    -> middleware (auth, logging, validation)
    -> handler/controller (business logic)
    -> service/model layer (data access)
    -> database query
    -> response construction
    -> HTTP response sent

Understanding one complete path through the system teaches you the architecture. Every other path follows roughly the same structure with different specifics.

The Dependency Graph

After the entry point, understand what depends on what. This tells you which pieces are central and which are peripheral.

Ways to find the dependency graph:
  1. Package manifest (package.json, requirements.txt, go.mod, Cargo.toml)
     - tells you external dependencies
  2. Import statements
     - tells you internal dependencies
  3. Build configuration (webpack.config.js, Makefile, tsconfig.json)
     - tells you build-time structure
  4. IDE "find all references" on key types
     - tells you runtime dependency flow

Not all files are equally important. In most codebases, 10-20% of the files contain the core logic that everything else depends on. Find those files first.

Signs a file is central:
  - Many other files import from it
  - It defines core data types or interfaces
  - It has been modified frequently (check git log)
  - It has the most test coverage
  - Other engineers mention it in design docs or comments

Tests As Documentation

Tests are often the best documentation in a codebase. They show you:

  • What the code is supposed to do (the assertions)
  • What inputs are considered normal (the setup)
  • What edge cases matter (the edge case tests)
  • How to use the public API (the test calls)
Reading tests strategically:
  1. Look at test file names to understand feature areas
  2. Read test names (describe/it blocks) as a behavior spec
  3. Look at test setup (beforeEach, fixtures) to understand
     required state
  4. Read assertions to understand expected behavior
  5. Look at mock setup to understand external dependencies

A test file named test_payment_processing.py with tests like test_charge_succeeds_with_valid_card, test_charge_fails_with_expired_card, test_charge_retries_on_gateway_timeout tells you more about the payment system than most design docs would.

Git Log As Archaeology

The git history is an underused tool for understanding code. It tells you not just what the code is, but how it got that way and why.

Useful git archaeology commands:

  # Who works on this file? (find the experts)
  git log --format="%an" -- path/to/file.py | sort | uniq -c | sort -rn

  # What changed recently? (find active areas)
  git log --oneline --since="3 months ago" -- src/

  # Why does this function look like this? (find the commit that shaped it)
  git log -p -- path/to/file.py

  # When was this line added and by whom?
  git blame path/to/file.py

  # What was the codebase like 6 months ago? (understand evolution)
  git log --oneline --all --graph --since="6 months ago"

  # Find when a specific function was introduced
  git log -S "def process_payment" --oneline

Git blame is particularly powerful. When you find a confusing line of code, git blame tells you who wrote it, when, and the commit message often explains why. If the commit message is "fix bug #1234," go read bug #1234.

Reading Configuration

Configuration files are often ignored during code reading, but they encode critical decisions:

Files to read early:
  - .env.example or .env.sample (what config does this app need?)
  - docker-compose.yml (what services does this depend on?)
  - CI/CD config (.github/workflows/, .gitlab-ci.yml, Jenkinsfile)
  - Database migrations (what is the schema?)
  - Infrastructure as code (terraform/, k8s/)

The Docker Compose file is secretly one of the best architecture documents. It shows you every service, every database, every queue, and how they connect.

Building a Mental Model

As you explore, you are building a mental model of the system. Make it explicit:

Questions to answer as you explore:
  1. What does this system DO? (one sentence)
  2. What are the major components? (3-7 pieces)
  3. How do they communicate? (HTTP, queues, shared DB, files)
  4. Where does data enter the system? (APIs, files, events)
  5. Where does data leave the system? (responses, emails, writes)
  6. What are the key data types? (User, Order, Payment, etc.)
  7. What external services does it depend on? (Stripe, S3, Redis)

Write these answers down. Even rough notes help you retain the mental model and give future newcomers a starting point.

The 30-Minute Orientation

When joining a new codebase, structure your first exploration:

Minutes 0-5:   Read the README and any docs/ directory
Minutes 5-10:  Read the package manifest and entry point
Minutes 10-15: Find and skim the test directory structure
Minutes 15-20: Follow one request end-to-end
Minutes 20-25: Check git log for recent changes and active areas
Minutes 25-30: Read the Docker/CI config for infrastructure context

After 30 minutes, you should be able to answer: "What does this system do, what is it built with, and where does the core logic live?" That is enough to start contributing.

Real-World Example: Joining a Microservices Team

An engineer joined a team with 8 microservices. The onboarding doc said "read the wiki." The wiki was 200 pages of stale documentation. Instead, the engineer:

1. Read docker-compose.yml -> found all 8 services and their connections
2. Found the API gateway service -> traced a request through 3 services
3. Read the most-modified service's tests -> understood the core domain
4. git log --since="2 weeks ago" across all repos -> found active work areas
5. Drew a box-and-arrow sketch of the services on paper

In one afternoon, they had a working mental model. Teammates who had been there for months had never drawn the architecture out and were surprised by some of the connections.

Common Pitfalls

  • Reading linearly from top to bottom — codebases are not books. Read them by following execution paths, not file order.
  • Starting with the most complex file — start with entry points and tests. They are designed to be understandable. Internal implementation files are not.
  • Ignoring the git history — the code tells you WHAT. The history tells you WHY. Use git blame and git log heavily.
  • Trying to understand everything before starting — you do not need to understand the whole codebase to be productive. Understand one path deeply, then expand.
  • Not asking the humans — code reading is faster with a guide. Ask a teammate "where does X happen?" and save yourself 30 minutes of searching.

Key Takeaways

  • Start with entry points, not the directory tree. Find where execution begins and follow one path end-to-end.
  • Tests are often the best documentation. They show intended behavior, edge cases, and usage patterns.
  • Git history is archaeology. git blame, git log, and git log -S tell you why code looks the way it does.
  • Configuration files (Docker Compose, CI config, env samples) encode architectural decisions. Read them early.
  • Build an explicit mental model: what the system does, its major components, how they communicate, and where data flows. Write it down, even roughly.