Reading CodeGENERALSTAGE-SPECIFICILLUSTRATIVE

Reading a Codebase

README, entry point, one main feature end to end, the dependencies it pulls in, the data model it touches, and the tests that describe it — in that order, and stopping when the question you came with is answered. Not every file.

The moveWorked exampleNext questions

The situation, the reflex, and why it stalls

Every lesson starts where being stuck starts: someone has a problem, and the first move that comes to mind feels like progress.

The question

You have just been given access to a repository you have never seen and a task inside it. Where do you start reading, and when do you stop?

The situation

First day on the store's team. The repository has hundreds of files, a README that says how to install it, and a ticket: "checkout shows Payment failed for some customers". You open the file tree and it is alphabetical.

The reflex

Read from the top of the tree, folder by folder, until it makes sense. It feels thorough — surely the way to understand a codebase is to read it — and it produces a steady feeling of coverage.

Why it stalls

The tree is alphabetical and the system is not. Reading auth/ before checkout/ because A comes before C teaches you about a subsystem your ticket does not touch, and the ticket is no closer.

What the reflex produces — and fails to produce
  • The tree is alphabetical and the system is not. Reading auth/ before checkout/ because A comes before C teaches you about a subsystem your ticket does not touch, and the ticket is no closer.
  • Files read in isolation do not say what calls them or why. A well-named OrderService read on its own is a list of methods; whether any of them run during checkout is not in the file.
  • Coverage is mistaken for understanding. After a day of reading, the honest summary is "I have seen most of it" and the question "what happens when a customer clicks Pay?" still cannot be answered without opening the editor again.
  • The slogan "read every file to understand it" is the reflex made explicit; made precise it is false for any codebase larger than a weekend project, because understanding is of paths and responsibilities, not of files, and paths cross files in an order the tree does not show.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

Precisely enough to apply it to a problem you have never seen — not a slogan.

  • Read with a question. "How does checkout decide a payment failed?" chooses what to open and says when to stop; "understand the codebase" chooses nothing and never stops. If you do not have a question, the first job is to get one — usually from the task.
  • Follow a fixed order that goes from orientation to specifics. The README for how to run it and what it claims to be; the entry point for where execution starts; one main feature traced end to end for how the layers connect; the dependencies that feature pulls in for what the code does not do itself; the data model for what persists; the tests for what the authors thought mattered.
  • Treat everything you do not yet know as an unknown to sharpen, not a gap to fill. "I don't understand the payment module" becomes "which function receives the provider's response and what does it do with a non-success status?" — a question with a place to look (Unknown to Specific Question).
  • Write the map as you go: the path through the layers for the feature you traced, the modules it touched, what each owns. The map is small, and it is what you would have wanted on day one; it is also what the next person gets.

The order, and when to leave it

The default order goes from the cheapest orientation to the most specific detail, and each step tells you what to look for in the next. It is not the only order; the alternative names a case where starting elsewhere is right.

Reading the store to answer "how does checkout decide a payment failed?"
  1. 1
    README, and run it

    because What it claims to be and how to make it a running system; a system you can click through is easier to read than a text.

  2. 2
    Entry point: bootstrap and route table

    because Where execution starts and how a request finds its handler — the skeleton every trace hangs from.

  3. 3
    One feature end to end: the pay request

    because One path through every layer teaches the conventions and answers the question; the second path is cheap once the first is known.

  4. 4
    Dependencies the feature pulls in

    because The provider SDK and the ORM do most of the work; knowing what is theirs keeps you from looking for it in the code.

  5. 5
    Data model for the tables touched

    because What persists is what the feature actually changes; the schema often explains behaviour the code leaves implicit.

  6. 6
    Tests for that feature

    because They record what the authors believed must hold, and their gaps are your unknowns.

a different valid order Schema-first: in a system whose behaviour is mostly data — a reporting service, an analytics dashboard — start with the tables and the queries that fill them, then find the code that writes to them. You would choose this when the question is about what the data means rather than what a request does, and the entry point tells you little.

The map as a decomposition

What the reading produces is a decomposition of the codebase along the path you traced, with a way to check each piece. The leaves are testable in the reading sense: the observation is "I can point at the code and run it", which is how you know the map is true rather than assumed.

The store, as read on day one
How the pay request is handled
  • Entrywhere a request becomes a handler call
    • Bootstrap and routertestable A request to /checkout/pay reaches payHandler — confirmed by a log line at the handler with the request id.
  • The pay paththe feature the ticket is about
    • payHandler → CheckoutService.paytestable Calling the service directly with a cart id produces the same result as the HTTP request.
    • PaymentGateway.chargetestable With the provider in test mode, a test card produces a charge visible in the provider dashboard.
    • OrderRepository.markPaidtestable After a successful charge the orders row has status paid and a payment row exists.
  • Outside the codewhat the dependencies own
    • Provider SDKtestable The SDK's documented response shape matches what PaymentGateway reads — checked against the version in the lockfile.
    • ORM and schematestable The orders and payments tables exist with the columns the repository writes — checked against the migration files.
  • Not yet readhonestly blank
    • auth, catalog, admin, email, uploadstestable Not on the traced path; each becomes a map entry when a task touches it.

The "not yet read" branch is deliberate. A map that pretends to cover the whole codebase after one day is the reflex again, with better formatting.

What is still unknown after the first day

Reading answers the question you came with and produces new ones. The board is the day-one output: what is known because it was traced and run, what is assumed because the README said so, and what became a specific question with a place to look.

After tracing the pay path
known
  • The pay request passes through four modules, one external call and two tables.
  • Any non-success from the charge, and any throw anywhere in the path, becomes the same 500 and the same "Payment failed" message.
  • Tests cover a successful charge and a decline.
assumed
  • ~The provider SDK version in the lockfile matches what the gateway code was written against — to be checked against the changelog, not the README.
  • ~Checkout is the only path that creates orders — the admin module has not been read and may have one.
unknown → question → experiment
  1. ? What happens on timeout?

    becomes When the provider call times out, does the gateway throw, return a failure, or return nothing — and is the order left in a state that can be retried?

    experiment Point the gateway at a stub that never responds, run the pay path, read the order row and the response.

  2. ? What about the double click?

    becomes Is there anything — a key, a constraint, a lock — that prevents two overlapping pay requests for one cart from creating two orders?

    experiment Send two concurrent pay requests for one cart in the test environment and count the orders.

  3. ? Where else do orders come from?

    becomes Does any module other than CheckoutService insert into the orders table?

    experiment Search the codebase for writes to the orders table and list the call sites.

The first two unknowns are the ticket. Reading did not solve it; it turned it into two experiments, which is what reading is for.

How to do it

Most important first.

  • Run it first. A codebase you can run and click through is a system; one you can only read is a text. The README's install steps are the first thing to verify.
  • Find the entry point — main, the server bootstrap, the route table — and read only far enough to see how a request reaches a handler (Debugging Code You Did Not Write).
  • Pick the feature your task touches and trace one request through it, top to bottom: route → handler → services → repository → table → response. Note every module you pass through and every external call (Follow the Data).
  • Open the dependency manifest and, for the feature you traced, name what each dependency is doing — the provider SDK, the ORM, the validation library — so that "the code" is separated from "the libraries".
  • Read the schema or the model definitions for the tables the feature touched. Then read the tests for that feature: they say what the authors believed had to hold (Characterization Tests says what to do when there are none).
  • Stop when your question is answered. Write down what you learned and the next question; the rest of the codebase waits until a task needs it.

Worked on a concrete problem

The move has to produce something. This is what it produced.

  • Question: how does checkout decide a payment failed? README: a web app with a single backend, a database, and a payment provider in test mode locally. Entry point: server.ts mounts a router; /checkout/pay maps to payHandler. Feature trace: payHandlerCheckoutService.payPaymentGateway.charge (wraps the provider SDK) → OrderRepository.markPaid → response. Four modules, one external call, one table. Dependencies pulled in: the provider SDK, the ORM. Data: orders, payments. Tests: checkout.test.ts covers a successful charge and a decline; nothing covers a timeout or a double click. The question is answered — any non-success from charge or any throw becomes the same 500 — and two unknowns are written down: what happens on timeout, and what happens on two overlapping requests.
  • What was not read: auth/, catalog/, admin/, the email module, the image upload path. None of them is on the traced path; all of them will be read when a task needs them. A day was spent on the checkout path and it is understood; the rest is a map with blank regions and no pretence otherwise.
  • The map, as written: a five-line list of the pay path with the module that owns each step, the two tables, the one external boundary, and the two unknowns. It went into the team wiki, and it was the most-read page there within a month, because everyone joining had the same question.

How you know it worked

What now exists that did not before, and what question you can now ask.

  • You can answer the question you came with, and you can name the files you did not read and why they did not matter to it.
  • You can trace one request end to end from memory: route, handler, services, storage, response.
  • You know which behaviour comes from the codebase and which from its dependencies.
  • A written map exists that is shorter than the code and longer than the README.

The questions you can now ask

The field this whole domain exists for. After this lesson, these are the questions to put to an unfamiliar problem.

Next questions
  • ?What question am I reading to answer, and what would answering it look like?
  • ?Where does execution start, and how does a request reach the feature I care about?
  • ?Which modules does one request through that feature pass through, and what does each own?
  • ?What does the code do itself, and what does it hand to a dependency?
  • ?What do the tests say the authors believed had to hold — and what do they not cover?

What can go wrong

How the move itself fails
  • Reading the whole feature trace for a task that only touches one function. The order is a default; a ticket that names a file and a line can start there and read outward only as far as the fix needs.
  • Trusting the README over the code. READMEs drift; "uses the provider's hosted checkout" may describe last year. Run it and trace it; the README is a hypothesis.
  • Tracing one feature and assuming the others are shaped the same. The checkout path was built carefully; the admin path may be a different generation of the codebase with different conventions (What "Legacy" Actually Means).
  • Never stopping. The question was answered an hour ago and the reading continues because the codebase is interesting; that is learning, which is fine, and it is not the task.
What the move costs
  • Question-driven reading leaves blank regions on the map. Someone who reads broadly learns things they were not looking for, and occasionally one of those is the cause of the bug you were tracing. The move is a default, not a prohibition on curiosity.
  • Tracing a feature end to end in an unfamiliar stack is slow at first, because each layer's conventions have to be learned as you cross them. It gets fast; the first trace is the expensive one.
  • Writing the map costs time that a solo engineer on a short task may reasonably skip; on a team it pays back with the next person.
Misreads
  • "So never read code without a task." Reading to learn is a different activity with a different stopping rule — curiosity — and it is valuable. This lesson is about reading to answer, where the stopping rule is the answer.
  • "The order is fixed." README → entry → feature → dependencies → data → tests is a default that goes from orientation to detail. A data-heavy system might start at the schema; a library might start at its public API and tests.
  • "Once I have the map I understand the codebase." You understand one path. The map says where the blank regions are; that is its honesty, not its failure.

Where this applies

Problem-solving advice is stated as universal far more often than it is. These labels say what each method is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALThe order — orient, find the start, trace one path, separate code from dependencies, find the data, read the tests — applies to a web app, a CLI, a library or a data pipeline; what counts as the entry point and the data changes.
  • STAGE-SPECIFICIn a young codebase the README is often accurate and the trace is short; in a legacy system the README describes an earlier architecture, several generations of convention coexist, and the map must record which generation each module belongs to.
  • ILLUSTRATIVEThe store's module names, the five-line map and the wiki page are invented to show the reading producing an answer; no real repository is described.

Where the depth lives

This domain asks the question and hands the answer off by name.

Further
  • The manifesto's delegation cards at /manifesto/delegating are useful on day one: for each dependency the trace crossed, what does it handle and what remains yours to understand?