Engineering Atlas › Problem Solving & Incidents (AI era)

Reading and Owning Code You Didn't Write

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The new legacy code

Michael Feathers defined legacy code as code without tests. A useful update for the AI era: legacy code is code nobody on the team understands, however new it is. Agents can produce a working service in an afternoon; if no human read it carefully, it becomes legacy the moment it merges. Inherited code, acquired code and agent code are the same problem: behaviour you depend on, with intent you cannot see.

Read with a goal

Reading a codebase file by file is like reading a dictionary to learn a language. Instead:

  1. Pick one real flow, such as "a user places an order", and follow it from the entry point (an HTTP handler, a queue consumer, a cron job) to the database and back.
  2. Run it locally or in staging with logging or a debugger, and watch the path rather than imagining it.
  3. Draw the map: components, data stores and calls on the critical path, in a C4-style context or container diagram. A page of boxes beats a hundred pages of notes.
  4. Find the edges: what external services it calls, what it writes, what it assumes about time, retries and failure.
  5. Then read details where the map shows risk: money, permissions, concurrency, data deletion.

Agents are useful readers here. Ask one to trace a flow and cite file paths and line numbers, then check the citations. An explanation without references is a guess.

Characterization tests

Before changing code you do not understand, write tests that capture what it does now. Call the function with realistic inputs, record the outputs, and assert them, even outputs that look wrong. Feathers' recipe: break dependencies so the code runs in a harness, write a test with a placeholder expected value, run it, read the real output from the failure, paste it into the assertion, rename the test to describe the behaviour, and repeat with new inputs. If a result surprises you, keep the test and investigate before calling it a bug (Feathers). These tests do not say the behaviour is correct; they say it has not changed. When you later fix a "bug", one test will fail, and you will decide on purpose whether that change is intended. Golden-file or snapshot tests (record a whole response or report and compare future runs against it) do the same job at the level of a service.

History as evidence

Code shows what; history shows why. git log -p --follow path shows how a file evolved; git blame -w -C attributes each line while ignoring whitespace and code moved between files (one -C looks at other files changed in the same commit, -C -C also at the commit that created the file, and three look at any commit) (git docs); the linked pull request often has the discussion that explains an odd if. A bulk reformatting commit hides real history, so list it in a .git-blame-ignore-revs file and set blame.ignoreRevsFile (git docs). Architecture decision records, where they exist, are the best source of all. Nygard's format is deliberately short, one or two pages with title, context, decision, status and consequences, because large documents are never kept up to date (Nygard). Agent-written code is often missing this layer: the reasoning lived in a chat session that is gone. When you work out why something is the way it is, record it in a comment, a decision record or the spec.

Knowledge risk

The bus factor is the number of people who would have to leave before nobody understands a part of the system. For critical paths it should be at least two. A simple proxy from version control: for each file, which current team members have committed to it? Files touched only by former members or only by agents have a bus factor of zero for practical purposes.

Down the stack

Layer 1The contract

Owning a component promises that a named team can explain its critical paths (at Google, ownership is a stewardship duty and owners are expected to understand the code they own, Software Engineering at Google, Ch 9), has tests that pin its current behaviour, can deploy and roll it back, and knows its dependencies and failure modes. A good test of ownership: could the on-call engineer debug it at night without the original author, human or agent?

Layer 2The mechanism

Version control stores what you need to measure ownership. Every commit records an author, a timestamp and the files changed; git log --format='%an' -- path | sort | uniq -c counts commits per author for a file. git blame walks history backwards to find the last commit that touched each line, and with -C detects lines moved or copied from other files. Agent commits can be marked by a trailer such as Co-Authored-By: or by a bot author name, so tooling can separate them. CODEOWNERS maps paths to owning teams for review; ownership dashboards join that map with commit history to show where owners have never touched the code they own.

Layer 3The machine

Git's history is a chain of immutable snapshots. Each commit object points to a tree object (a directory listing of file names and content hashes) and to its parent commits, and every object is named by the hash of its content (SHA-1 or SHA-256). Diffs are not stored; they are computed by comparing trees, which is why git blame on a large file with long history can take seconds: it repeatedly diffs a file's versions back through time. Because the hashes chain, history cannot be silently edited without changing every later commit ID, which is what makes it trustworthy evidence about who changed what and when.

Why it's built this way

  1. Teams depend on code whose authors are gone, whether people or agent sessions.
  2. Behaviour can be observed and pinned, even when intent is lost, through characterization tests.
  3. Intent can often be recovered from history and discussion, if it was recorded.
  4. Understanding concentrated in one head, or in no head, is fragile.
  5. So: read by flows, pin behaviour, mine history, record the why, and measure knowledge risk.

Diagram

flowchart LR
  E["Entry point"] --> F["Follow one real flow"]
  F --> MAP["Draw the critical path map"]
  MAP --> CT["Characterization tests pin behaviour"]
  MAP --> H["git log, blame, PRs: recover the why"]
  CT --> CH["Change safely"]
  H --> DOC["Record decisions"]
  CH --> OWN["Two people can explain it"]
  DOC --> OWN

Worked example

A platform team inherits a billing reconciliation service after its author leaves. It is 14,000 lines; git shows 80 percent of lines were last touched by agent commits during a three-week sprint, and none of the five current team members has ever committed to it. It runs nightly and compares 2 million ledger rows with the payment provider's report.

The team follows one flow: the nightly job's entry point, the provider download, the matching step and the mismatch report. Running it against a copy of last night's data takes 11 minutes and finds 37 mismatches, which matches production. They draw the map: downloader, parser, matcher, report writer, and a little-known retry loop that re-fetches the provider file if it is under 1 MB.

They write characterization tests: a golden test over a 10,000-row sample that records the exact mismatch report, plus tests around the retry loop. git blame on the retry loop leads to a pull request where the agent's summary says "handle partial downloads", which explains the 1 MB rule: the provider once served truncated files. They write that into a decision record.

Two weeks later, a change to the parser breaks the golden test: amounts with three decimal places, used for one currency, were now rounded. Without the characterization test this would have shown up as a month-end discrepancy in finance.

In the wild

Trade-offs

ApproachStrengthsWeaknessesChoose when
Follow one flow end to endFast understanding of what mattersMisses rarely used pathsFirst days with any codebase
Read everythingCompleteSlow, low retentionSmall codebases only
Characterization tests firstSafe changes even without understandingPins bugs as well as featuresBefore any change to unknown code
Ask an agent to explainFast orientationCan be confidently wrongAlways with file and line citations, checked
Rewrite from scratchClean resultLoses undocumented behaviour, high riskRarely; prefer gradual replacement

Pitfalls in production

Leadership lens

Check yourself

  1. Why are characterization tests useful even when they pin behaviour that may be wrong?

    Show answer

    They make any change in behaviour visible, so fixes are deliberate decisions rather than accidents, and unknown dependencies on current behaviour surface before production.

  2. Why start reading from one real flow rather than from the top of the repository?

    Show answer

    A flow shows the code that actually runs and matters, in the order it runs, and gives you a map to hang details on; reading files in order shows lots of code with no context.

  3. What makes agent-written code a particular knowledge risk?

    Show answer

    It can be produced quickly and in large amounts with no human ever reading it closely, and the reasoning behind it often lived only in a chat session that is gone.

  4. What does git blame -C add?

    Show answer

    It detects lines moved or copied from other files, so attribution points to where the code was really written rather than to the commit that moved it.

Sources and further study

Primary

Books

Other curricula

Free resources

Open in the interactive app: practice, quiz, mock interview →