codeintelligently
Back to posts
Codebase Understanding

Reading Code: The Skill Nobody Teaches

Vaibhav Verma
7 min read
code readingdeveloper skillscodebase understandingsoftware engineeringlearning

Reading Code: The Skill Nobody Teaches

I got a computer science degree. I attended two bootcamps. I've read probably 40 programming books. Not a single one taught me how to read code. They all taught me how to write it. Which is bizarre, because the data says I spend more than half my working hours reading code, not writing it.

The University of Zurich published a study in 2019 using eye-tracking technology. They found that developers spend 58% of their time on code comprehension activities. Not typing. Not designing. Not debugging. Reading and understanding existing code. We spend the majority of our professional lives doing something we were never trained to do.

I spent my first five years as a developer reading code badly. I'd start at the top of a file, read every line, and try to build a mental model as I went. It was slow, exhausting, and I forgot most of what I read by the next day. Then I studied how expert developers read code, and everything changed.

How Experts Read Code (It's Not What You Think)

Researchers at the University of Zurich and the University of Passau used eye-tracking studies to compare how novice and expert developers read code. The differences were dramatic:

Novices read linearly: top to bottom, left to right, like prose. They spend equal time on every line. They rarely jump between files.

Experts read strategically: they scan for structure first, then dive into specific areas. They jump between files frequently. They spend 80% of their time on 20% of the code. They read function signatures more than function bodies.

The key insight: expert developers don't read code. They interrogate it. They come with questions and search for answers. Novices come with no questions and hope understanding will emerge from reading.

The Code Reading Framework

Based on the research and my own experience, here's the systematic approach I now use and teach to every developer I mentor.

Phase 1: Orientation (5 Minutes)

Before reading a single line of implementation code, answer these questions:

  1. What is this file/module supposed to do? Read the file name, class name, and any top-level comments. If there's a README in the directory, read it.
  2. What does it depend on? Read the import statements. Group them: standard library, third-party, internal. The imports tell you the vocabulary this code uses.
  3. What does it export? Scan for exported functions, classes, or constants. These are the public API. This is what the rest of the codebase sees.
  4. How big is it? Glance at the line count. A 50-line file requires a different reading strategy than a 500-line file.
typescript
// Phase 1 in action: reading a new file

// 1. File name: OrderService.ts -> handles order operations
// 2. Imports:
import { prisma } from "../db";              // Database access
import { EventBus } from "../events";         // Event publishing
import { InventoryService } from "./inventory"; // Inventory checks
import { z } from "zod";                      // Input validation
// 3. Exports:
export class OrderService { ... }
export type OrderInput = z.infer<typeof orderSchema>;
// 4. Size: 280 lines -> medium, probably 5-8 methods

In 5 minutes, before reading a single line of implementation, you already know: this is an order service that talks to the database, publishes events, checks inventory, and validates input with Zod. That context makes everything else easier to understand.

Phase 2: Structural Scan (10 Minutes)

Now scan the structure without reading implementation details:

  1. Read every function/method signature. Name, parameters, return type. Skip the body.
  2. Identify the main flow. Which function is the entry point? Which functions does it call?
  3. Spot the patterns. Is there a consistent pattern? (validate -> process -> persist -> publish)
  4. Mark the interesting parts. Flag anything unusual, complex, or poorly named for later deep reading.
typescript
// Phase 2: reading only signatures

class OrderService {
  async createOrder(input: OrderInput): Promise<Order> { ... }
  async cancelOrder(orderId: string, reason: string): Promise<void> { ... }
  async getOrder(orderId: string): Promise<Order | null> { ... }
  async listOrders(filters: OrderFilters): Promise<PaginatedResult<Order>> { ... }
  private async validateInventory(items: LineItem[]): Promise<void> { ... }
  private async calculateTotal(items: LineItem[]): Promise<Money> { ... }
  private async applyDiscounts(total: Money, codes: string[]): Promise<Money> { ... }
}

// Now I know: 4 public methods (CRUD-ish), 3 private helpers
// createOrder probably calls validateInventory -> calculateTotal -> applyDiscounts
// I'd read createOrder first since it's the most complex flow

Phase 3: Targeted Deep Reading (As Needed)

Now pick the function you actually need to understand and read it carefully. But not linearly. Use this order:

  1. Read the happy path first. Ignore error handling, edge cases, and defensive checks on the first pass. Follow the main flow from input to output.
  2. Read the error handling second. Now go back and read the validation, the try/catch blocks, the early returns. These tell you what can go wrong.
  3. Read the edge cases third. The conditional branches, the special cases, the TODOs. These tell you about the system's history and its battle scars.

Five Techniques That 10x Your Reading Speed

Technique 1: The Rename Trick

When you encounter a poorly named variable or function, mentally rename it. Don't actually refactor it (yet). Just translate it in your head. d becomes discountPercentage. proc becomes processPaymentTransaction. This tiny act of translation forces active comprehension instead of passive reading.

Technique 2: The Question Stack

Keep a running list of questions as you read. Write them down. Don't try to answer them immediately. Just collect them. After reading a function, go through the list and see which ones you can now answer. The unanswered questions guide your next reading.

My question stack for a typical function:

- Why is there a 500ms delay before the retry?
- What happens if validateInventory throws?
- Why is calculateTotal async? (DB call for tax rates?)
- What's the difference between "cancelled" and "voided"?

Technique 3: Call Graph Tracing

When you hit a function call you don't understand, don't immediately jump to it. Write down the call and keep reading the current function. After you finish, decide which calls you actually need to understand and investigate those. This prevents the "rabbit hole" problem where you jump to a function, which calls another function, which calls another, and you've lost all context.

Technique 4: Type-First Reading

In typed languages, read the types before the logic. Types are a form of documentation that's verified by the compiler. A function that takes an Order and returns a ShippingLabel tells you 80% of what you need to know before you read a single line of implementation.

typescript
// The types tell you almost everything
function calculateShipping(
  order: Order,
  destination: Address,
  carrier: ShippingCarrier,
  options: ShippingOptions
): Promise<ShippingQuote> {
  // By this point, you know: it takes order details, a destination,
  // a carrier choice, and some options. It returns a quote.
  // The implementation is just the "how."
}

Technique 5: Test-First Reading

When a file has a corresponding test file, read the tests first. Tests show you how the code is supposed to be used, what inputs it expects, and what outputs it produces. They're executable documentation.

typescript
// Reading this test tells you more than reading the implementation
describe("OrderService.createOrder", () => {
  it("creates an order with valid items and sufficient inventory", ...);
  it("throws InsufficientInventory when stock is too low", ...);
  it("applies discount codes and reduces the total", ...);
  it("publishes an order.created event after successful creation", ...);
  it("rolls back the order if payment processing fails", ...);
});
// Now you know the happy path, two error cases, and two side effects

The Code Reading Practice Plan

Like any skill, code reading improves with deliberate practice. Here's a 30-day plan:

Week 1: Read one unfamiliar file per day. Pick a file from a module you don't work in. Apply Phases 1-3. Timebox to 30 minutes. Write a one-paragraph summary of what the file does.

Week 2: Trace one user action per day. Pick a feature. Trace it from the API endpoint through the business logic to the database query. Draw the call graph on paper.

Week 3: Read one open-source project per day. Pick a small, well-regarded project (under 5,000 lines). Apply the framework. Reading other people's code in different styles stretches your comprehension muscles.

Week 4: Read with a partner. Pair code-reading sessions where you and a colleague read the same code and compare interpretations. You'll be surprised how differently two people interpret the same function.

My Contrarian Take: Reading Code Is More Important Than Writing It

I was wrong about this for most of my career. I thought the value of a developer was in what they could build. I now believe the value is in what they can understand. The developer who deeply understands the system makes better decisions about what to build, where to build it, and what not to build.

The best senior engineers I've worked with aren't the fastest typists or the most prolific committers. They're the ones who read a 10,000-line module and say, "This entire subsystem can be replaced with 200 lines if we change the data model." That insight comes from reading, not writing.

The Code Reading Checklist

Use this for every non-trivial reading session:

  • Read the file/module name and form an expectation
  • Read all import statements and categorize them
  • Read all export statements to identify the public API
  • Read every function signature (skip implementations)
  • Identify the entry point and main flow
  • Read the happy path of the most important function
  • Read the error handling
  • Check if tests exist and read the test names
  • Write a one-paragraph summary
  • List remaining questions

Teach yourself to read code. It's the highest-leverage skill in software engineering, and you probably haven't spent a single hour practicing it deliberately. Start today.

$ ls ./related

Explore by topic