Every developer I know has a system for writing code. Almost none have a system for reading code. This is backwards. You spend way more time reading code than writing it — some estimates say 10:1. And yet we treat reading as passive, as something you just... do.
It's not passive. It's a skill. Here's how I approach it.
Phase 1: Ignore the Code
This sounds paradoxical, but the first thing I do with a new codebase is not look at the code. Instead:
Read the README. Not the whole thing — just enough to understand what the project does and how to run it.
Look at the file tree. Not the files — the tree. The directory structure tells you more about the project's architecture than any diagram.
src/
├── app/ ← routes (Next.js app router)
├── components/ ← UI building blocks
├── config/ ← constants and settings
└── lib/ ← shared utilities
From this alone, I know: it's a Next.js app. The team separates UI from logic. Config is centralized. There's probably not a lot of abstraction — four directories is restrained.
Check the dependency list. package.json, Cargo.toml, requirements.txt — whatever it is. The dependencies tell you what problems the team decided not to solve themselves.
{
"dependencies": {
"next": "^15", // framework
"react": "^19", // UI library
"velite": "^0.3", // content layer
"clsx": "^2", // conditional classes
"tailwind-merge": "^2" // class merging
}
}
Five runtime dependencies. This is a small, focused project. No state management library means state is simple. Velite means content is file-based. The team values simplicity.
All of this before reading a single line of code.
Phase 2: Follow the Data
Now I read code, but not top-to-bottom. I follow the data.
Pick the most important entity in the system. In a blog, that's a post. In an e-commerce app, that's an order. In a chat app, that's a message. Then trace it from birth to death:
- Where is it created? (Database? File system? API?)
- How is it transformed? (Validation? Enrichment? Formatting?)
- Where is it displayed? (Which component renders it?)
- How does the user interact with it? (Edit? Delete? Share?)
Post lifecycle in this codebase:
content/posts/*.mdx → Velite reads and parses
↓
.velite/posts.json → Compiled content with metadata
↓
src/lib/content.ts → Getter functions (getAllPosts, getPostBySlug)
↓
src/app/posts/[slug]/ → Dynamic route renders the post
↓
src/components/post-* → PostHeader, PostBody display it
This gives you the spine of the application. Everything else is supporting structure.
Phase 3: Find the Weird Parts
Every codebase has weird parts — code that doesn't follow the patterns established elsewhere. These are the most important parts to understand, because they represent places where the obvious solution didn't work.
Look for:
- Comments that say "why" (not "what"). A comment like
// We use setTimeout here because...marks a hard-won lesson. - Unusually long functions. If every function is 20 lines and one is 200, that function is doing something complex. Read it carefully.
- Configuration that seems excessive. Over-configured code is code that's been burned by under-configuration.
- Try-catch blocks. Every try-catch is a story about something that went wrong at least once.
// This is interesting code — it tells a story
// eslint-disable-next-line @typescript-eslint/no-explicit-any
apply(compiler: any) {
// Why "any"? Because webpack's TypeScript types don't
// expose the hooks we need. This is a known gap.
compiler.hooks.beforeCompile.tapPromise(...)
}
The any type is a scar. It marks a place where the type system couldn't express reality. Understanding why is more valuable than fixing it.
Phase 4: Run It and Break It
Once I have a mental model, I verify it by breaking things. Change a key function to return null. Delete an import. Rename a file. The errors tell you what depends on what — they're a map of the system's connections.
This is especially useful for understanding implicit dependencies. In a blog:
- Delete a content file → build error shows how content is loaded
- Remove a component export → compile error shows who uses it
- Change a route → 404 shows the routing structure
You learn more from errors than from success.
Phase 5: Draw It
Finally, I draw a diagram. Not before reading — after. The diagram isn't a plan; it's a summary of understanding. If I can draw the system from memory, I understand it. If I can't, I know what to read next.
The diagram doesn't need to be fancy. A box for each major concern. An arrow for each data flow. A note for each "weird part."
This is the step most people skip. It's also the step that transforms reading from temporary understanding into permanent knowledge.
The Meta-Skill
The real skill isn't any of these phases. It's knowing when to zoom in and when to zoom out. When to read every line and when to skip ahead. When to understand deeply and when to accept "this works, I'll learn why later."
Reading code is navigation. The codebase is a city. You don't need to walk every street. You need to know the landmarks, the main roads, and the neighborhoods. The side streets can wait until you need them.