Reading CodeGENERALTEAM-SPECIFICILLUSTRATIVE

Debugging Code You Did Not Write

Find the entry point, follow the data, find the state, find the side effects, understand the boundary. Five questions that turn an unfamiliar failing system into a path you can put observations on — without first understanding all of it.

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

Something is failing in code you have never read and there is no time to learn it all. What do you find first, and in what order?

The situation

Payment failed, in a codebase you joined last week. You know where the message is rendered and nothing else. The person who wrote the checkout module has left, and the ticket is yours.

The reflex

Search the codebase for the error text and start reading around every hit. It feels targeted — the message is the one concrete thing you have — and it gives you a file to open.

Why it stalls

The message is rendered in one place and produced by any of a dozen causes; reading around the render site teaches you the UI's error handling and nothing about which failure occurred.

What the reflex produces — and fails to produce
  • The message is rendered in one place and produced by any of a dozen causes; reading around the render site teaches you the UI's error handling and nothing about which failure occurred.
  • Without the entry point, you cannot say which code runs for a pay request and which is dead, so every file that mentions payments looks equally relevant.
  • Without knowing where state lives, you cannot tell whether the failure is in a computation or in a stale row; without knowing the side effects, you cannot tell whether the provider was called; without the boundary, you cannot tell whether the failure is even yours.
  • The session becomes "understand the whole checkout module before touching anything", which is reading without a question, and the ticket waits.
ProblemUnderstandRequirementsConstraintsUnknownsDecompositionSmallest StepModelExperimentObserveDebugLearnIterate

The move

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

  • Ask five questions of the code, in an order where each answer narrows the next. Where does execution enter for this request? What data comes in, and where does it go? Where is state read and written? What side effects happen — external calls, writes, messages? Where is the boundary between this code and what it depends on?
  • Each answer gives you a place to put an observation. The entry point gives a first log line; the data path gives points to inspect a value; the state gives rows to read; the side effects give dashboards to check; the boundary gives the first place the failure might not be yours.
  • Then debug as in Debugging Is Problem Solving: hypothesis, prediction, cheapest distinguishing observation. The five questions do not find the bug; they build the map the debugging loop runs on, and they build only as much of it as the request touches.
  • Whatever you learn, write onto the map. Debugging unknown code is reading it with a purpose, and the map is what stays after the ticket closes.

Five questions, in an order that narrows

The order is not arbitrary. The entry point tells you which data to follow; the data path shows you where state is touched; the state and side effects together show where the path leaves the process; and the boundary is where responsibility — and usually the cause — changes hands.

Mapping the failing request
  1. 1
    Entry point

    The route, handler or job that runs for this request; one log line to prove it.

    fails by Starting at the render site of the message, which every cause passes through.

  2. 2
    Follow the data

    Input → each transformation → output; the assumption at each hand-off.

    fails by Reading functions in file order instead of call order.

  3. 3
    Find the state

    Tables, caches, in-memory structures read or written; the actual rows for the failing case.

    fails by Assuming the code is stateless because no variable is named "state".

  4. 4
    Find the side effects

    Every call that leaves the process, and the evidence each leaves behind.

    fails by Checking only the side effects you already suspected.

  5. 5
    Understand the boundary

    Which steps are yours and which belong to a dependency or an external system; on which side is the data first wrong.

    fails by Stopping at "it is the provider" without checking what we sent it.

Each step produces a place to put an observation. The debugging loop then chooses among them by how much each distinguishes.

The path, with the observation points marked

Drawn for the store's pay request, the map is small: an entry, a handful of transformations, two tables, one external system. Every node is somewhere an observation can be made, and the boundary nodes are where the question "is this ours?" gets asked.

cart iditems JSONcart objectamountboundarystatus, charge idside effectPOST /checkout/pay → payHandlercarts rowload cart (ORM)build order linesPaymentGateway.chargeprovider (SDK)mark order paidorders / payments rowssend confirmation
UserLLMAgentToolDataDecisionHumanGuardrail

Where the reading goes wrong

Each of the five questions has a way of being answered badly, and each bad answer sends the debugging loop to the wrong place. The table pairs them.

Bad answers to the five questions
TriggerSymptomCauseResponse
Entry point taken to be the render siteHours in the UI error handlingThe message is the last step of every failure, not the first of anyFind the handler for the request; put the first observation there.
Data followed by file, not by callA list of functions with no orderThe tree is alphabetical; the data path is notStart at the entry and follow each call; ignore files not on the path.
State assumed from the codeThe "impossible" inputThe row was written by an older version of the code or by another moduleRead the actual rows for the failing case (Source of Truth).
Side effects checked selectivelyThe provider is blamed; the provider was never calledOnly the suspected side effect was verifiedList every call that leaves the process and check each one's record.
Boundary treated as a verdictTicket closed as "provider issue"; customers still failingThe far side of the boundary was located and the near side's handling of it was not readAsk what our code does with the far side's answer (Treating External Systems as What They Are).

How to do it

Most important first.

  • Entry point: find the route or handler for the failing request, not the render site of the message. Put a log line with the request id there and confirm it fires for the reproduction.
  • Follow the data: read the input at the entry, then follow it through every transformation until the response. At each hand-off, ask what shape the next function assumes (Follow the Data).
  • Find the state: which tables, caches or in-memory structures the path reads and writes. Read the actual rows for the failing request's order and cart (What Information Changes Over Time?).
  • Find the side effects: every call that leaves the process — the provider, email, a queue. Check each one's record of whether it was reached.
  • Understand the boundary: which of the above are yours and which belong to a dependency or an external system. The first boundary where the data is already wrong is where the reading stops and the cause is near (Where Does My System End?).

Worked on a concrete problem

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

  • Entry: /checkout/paypayHandler. Data: the request carries a cart id; the handler loads the cart, builds order lines, calls the gateway, marks the order. Log at entry confirms it fires. State: carts, orders, payments; the failing request's cart row exists and its items column holds a JSON object keyed by product id. Side effects: the provider call and an email. Provider dashboard: no attempt. Boundary: the ORM is loading the cart; the gateway wraps the SDK. The data is already the "wrong" shape when it leaves the ORM — so the question moves to whoever writes the cart, which is the cart module, which was refactored last week.
  • The same five questions on a different failure: "Payment failed" only for customers with a saved card. Entry: same handler. Data: a saved-card token replaces the card fields. State: payment_methods table. Side effects: the provider is called and answers with a token-expired code. Boundary: the provider's tokens expire and ours are never refreshed. The bug is on our side of the boundary, but it is a policy gap rather than a code defect, and the fix is a decision about refreshing, not a patch.
  • What was not read in either case: the cart UI, the admin, the catalog, the rest of the checkout module. Two tickets closed in a codebase still mostly unread, with the map two entries longer.

How you know it worked

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

  • You can name the entry point, the data path, the state, the side effects and the boundary for the failing request, and you have an observation at each.
  • You know which parts of the path are yours and which belong to a dependency or a provider.
  • The debugging loop is running on a map rather than on a file tree.
  • The parts of the codebase you did not read are named as not read, not assumed understood.

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
  • ?Where does execution enter for this request, and can I confirm it with one observation?
  • ?What data comes in, what is it transformed into, and what does each step assume about its input?
  • ?Where is state read and written on this path, and what do the actual rows say for the failing case?
  • ?Which side effects should have happened, and did each one?
  • ?Where is the boundary between my code and what it depends on, and on which side is the data first wrong?

What can go wrong

How the move itself fails
  • Answering the five questions for the whole module instead of for the failing request. The questions are about one path; the module is context you may never need.
  • Skipping the state question because the code "looks stateless". A handler that reads a cart row is stateful at the row, and the row is where a stale shape hides.
  • Stopping at the boundary. "The data is wrong when it comes out of the ORM" is a finding, not a cause; the cause is on the writing side, and the reading continues there.
  • Not recording. Two tickets later the same five answers are rediscovered from scratch by the next person, or by you.
What the move costs
  • Five questions asked properly take longer than one lucky search. When the message is specific and the hit is the cause, the search wins.
  • The map built this way is shaped by the tickets you happened to get; it is accurate where it exists and blank elsewhere, and it never becomes the overview a designer would draw.
  • Debugging by boundaries can stop at "not our side" prematurely; a provider returning an unexpected code is still a case our code has to handle, and the ticket is not closed by locating the cause outside.
Misreads
  • "This is just reading a codebase with a bug attached." The order differs: reading to understand starts at the README; reading to debug starts at the entry point for one request and reads outward only as far as the failure requires.
  • "Finding the boundary means finding the culprit." The boundary is where responsibility changes hands. The data being wrong on the far side tells you where to read next; it is not a verdict.
  • "I should understand the module before I change it." You should understand the path you are changing and what it touches. The module as a whole is a different, larger question.

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.

  • GENERALEntry, data, state, side effects, boundary — the same five for a web handler, a batch job, a UI component or a CLI; what counts as entry and state changes.
  • TEAM-SPECIFICOn a team where the author is available, the fastest first observation is a question to them; the five questions are still what you ask, but the answers come in a conversation, and the map is theirs to correct (Asking People).
  • ILLUSTRATIVEThe departed author, the JSON cart column and the expired saved-card token are invented to show the five questions producing a place to look; no real system is described.

Where the depth lives

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

Observability & Performancetrace-anatomyincident-debugging
Further
  • The manifesto's layers page at /manifesto/layers is the same five questions asked of a stack rather than of a request: where does control enter, what does each layer hand to the next, where does it leave your code.