</>CodeWithKarani

How to Read a Codebase You Did Not Write, Without Panicking

Karani GeoffreyKarani Geoffrey8 min read

Panic is a symptom of reading in the wrong order

Here is the feeling. You clone a repository with 900 files. You open the folder tree, see forty directories with names like core, common, utils, services, legacy, and your brain quietly informs you that you are not smart enough for this job. Then you open a file at random, do not understand it, and conclude you were right.

You were not right. You were reading in the wrong order. A codebase is not a book and it is not a tree; it is a machine, and machines are understood by watching them run, not by reading their parts list.

The thesis: never try to understand a codebase. Try to answer one specific question about it, and let the understanding be a side effect. This is the difference between a week of anxious scrolling and a productive first afternoon.

Rule 1: run it before you read it

Do not read a single line until the thing runs on your machine. Not because running it teaches you the architecture, but because the process of getting it running teaches you more than the architecture would.

By the time it boots, you have learned the language and runtime versions, the external services it needs, which config is required versus optional, which parts of the README are lies, and where the setup pain lives. That last one is diagnostic: a project that takes two days to run locally is telling you something true about how it was maintained.

Look for these in order:

ls -a                       # .env.example, .tool-versions, Makefile, compose.yaml
cat Makefile                # often the real documentation
cat compose.yaml
cat .github/workflows/*.yml # CI is the honest setup guide; it must actually work

The CI workflow is the most underrated file in any repository. The README is aspirational and stale. CI is executable and green, therefore true. If you cannot work out how to run the tests, read the workflow file - it is literally a script for setting the project up from nothing.

Write down every step that was not documented, and open a PR fixing the README before you touch anything else. It is a genuinely useful contribution, it is low risk, and it gets you through the review and deploy process once while the stakes are zero.

Rule 2: find the edges, not the centre

There is no "centre" of a codebase, which is why looking for one feels so bad. There are edges: the places where the outside world gets in. Everything else is reachable from an edge.

The edges are always one of a small number of things: HTTP routes, CLI commands, scheduled jobs, queue consumers, webhook handlers, and database triggers. Find them mechanically:

# Routes
rg -n "@(app|router)\.(get|post|put|patch|delete)" --type py
rg -n "router\.(get|post|put|patch|delete)\(" --type ts
rg -n "path\(|re_path\(" --type py    # Django urls.py

# Scheduled work and background jobs
rg -n "cron|schedule|celery|sidekiq|bullmq|@Scheduled" -i

# Entry points
rg -n "if __name__ == .__main__." --type py
rg -n '"(main|start|dev)":' package.json

Now get a rough map of size and shape:

# Where is the code, and how much of it is there
tokei .          # or: cloc .

# The 20 largest source files, which are usually the important ones
find . -name "*.py" -not -path "*/.venv/*" \
  | xargs wc -l | sort -rn | head -20

Large files are not automatically bad, but in an unfamiliar codebase they are where the domain logic accumulated. That is where you want to be.

Rule 3: trace exactly one request all the way down

This is the core technique and the one that replaces panic with a model. Pick one thing the system does - preferably the most business-critical thing, like "a user completes a payment" - and follow it from the edge to the database and back, writing down every hop.

Do not read adjacent code. Do not follow interesting detours. Resist opening the utils file. One path, end to end.

Then do the crudest possible thing to verify you are right: add a print statement. Seriously.

import logging
log = logging.getLogger(__name__)

def process_payment(order_id, amount_minor):
    log.warning("TRACE process_payment order=%s amount=%s", order_id, amount_minor)
    ...

Sprinkle five or six of these along the path you believe the code takes, run one request, and look at the order they print in. You will be wrong about at least one hop. That wrongness is the most valuable information you will get all week, because it is exactly where your mental model diverged from reality - which is where the bugs live too.

A debugger is better where you can use one comfortably. Set a breakpoint at the deepest point of the path and read the call stack: that stack is the trace, generated for you, guaranteed accurate.

Rule 4: the data model is the real documentation

Code lies about intent. Schemas rarely do, because they have to be true for the system to run at all. After tracing one request, go read the schema:

# Straight from a live database
psql "$DATABASE_URL" -c "\dt"              # list tables
psql "$DATABASE_URL" -c "\d+ payouts"      # one table in detail

# Which tables actually carry data?
psql "$DATABASE_URL" -c "
SELECT relname, n_live_tup
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 25;"

That last query is a shortcut worth remembering. Row counts tell you which tables are load-bearing and which are abandoned experiments. A table with 4 million rows and a table with 6 rows are both in the schema, but only one of them is the product.

Sketch the six or seven core entities and their relationships on paper. Not the whole schema - the part that showed up in the request you traced. Once you can name the nouns, most of the code stops looking arbitrary, because it is mostly moving those nouns around.

Rule 5: use Git as an archaeologist

Every confusing piece of code was written by someone solving a problem you cannot see. Git remembers the problem.

# Why does this line exist? -w ignores whitespace, -C follows moved code
git log -p -L 120,140:src/payments/settle.py
git blame -w -C -C src/payments/settle.py

# When was this weird constant introduced, and in what commit?
git log -S "RETRY_BACKOFF_SECONDS" --oneline

# Which files change most often? Those are the hot spots you will work in.
git log --since="1 year ago" --name-only --pretty=format: \
  | sort | uniq -c | sort -rn | head -20

# Who knows about this file, if they are still around?
git shortlog -sn -- src/payments/

The churn command is the one I run on every new codebase. The top twenty files by change frequency are, in practice, the files you will spend your next six months in. Read those first and skip the other 880.

And if something is broken and you have a known-good point in history, do not reason about it. Let Git find it:

git bisect start
git bisect bad HEAD
git bisect good v2.4.0
# repeat: run the check, then mark
git bisect good   # or git bisect bad
git bisect reset

Rule 6: change something small and ship it in week one

Reading has diminishing returns fast. The step from 30 percent understanding to 60 percent does not come from more reading; it comes from making a change and finding out what you were wrong about.

Pick something with an obvious correct answer and a visible result: a copy fix, a missing validation, a log line that should include the request ID. Ship it. In the process you will learn the review conventions, the test setup, the deploy pipeline and who actually responds on the team - four things no amount of code reading would have taught you.

Rule 7: write down the map you wish had existed

Keep a scratch file from your first hour. Not a formal document. Just:

  • How to run it, including the undocumented steps.
  • The five to seven core entities and how they relate.
  • The one request you traced, hop by hop.
  • The seams: where the code talks to the outside world (payment provider, SMS gateway, storage).
  • Questions you could not answer, with the file and line where you got stuck.

That last list is the most valuable thing you own in week one. Take it to whoever knows the system and go through it in twenty minutes. Specific questions anchored to line numbers get real answers. "Can you walk me through the architecture" gets you a whiteboard drawing that is wrong in the ways that matter.

Two things not to do

Do not refactor in your first month. Every piece of ugly code is either load-bearing for a reason you have not discovered, or genuinely dead. You cannot tell which yet, and the failure mode of guessing wrong is deleting the workaround for a bug that comes straight back. Write the ugliness down instead. Revisit the list in month two, when half the entries will have explained themselves.

Do not read the whole thing. You will not finish, you will retain almost none of it, and the parts you retain will be arbitrary. Nobody on the team has read the whole thing either. They have deep knowledge of the ten percent they work in and vague gestures about the rest, and they ship fine.

Competence in an unfamiliar codebase is not knowing where everything is. It is knowing how to find anything within ten minutes.

That skill is transferable, it compounds across every job you will ever have, and it takes about a week to build in any given repository. The panic is optional.

#engineering-craft#onboarding#git#legacy-code#career
Keep reading

Related articles