Legacy

How do I understand a legacy codebase?

Seven passes, in an order that converges. The first one is not about the code.

Short answer

Do not start by reading the code. Start with what runs and where, then get an inventory of the parts, then the outside edges, then follow the data, then write down what could not be established — and only then read selectively, guided by the gaps. Change nothing structural until all of that exists. The output you are working towards is not a document; it is a short, prioritised list of questions nobody in your company can currently answer.

#Why the order matters

Almost everybody starts in the wrong place, and the wrong place is the code.

Reading a large unfamiliar codebase file by file feels like progress and is not. After a month you have a partial picture, it exists only in your head, nobody else can use it, and you cannot tell how much is left — which is the specific thing that makes legacy work feel endless.

The order below inverts that. Each pass produces something concrete, in a form somebody else can read, and each one narrows what the next pass has to look at. By pass five you have a bounded list of questions instead of an unbounded fear.

#The seven passes

  1. What runs, and where

    Before any code: which environments exist, what is deployed to each, what runs on a schedule, and what runs that is not in the repository at all.

    The last one catches everybody. Cron on a server, a cloud function created in a console, a script on a laptop, a spreadsheet somebody maintains by hand. Check the cloud console and the crontabs, not just the code.

  2. The inventory

    Every component with a type and one readable sentence about what it is for. Services, modules, databases, queues, scheduled jobs, front ends, external systems.

    This is the flattest artefact you will produce and the one that makes every subsequent conversation possible. It is also usually the moment somebody finds a component nobody knew existed.

  3. The outside edges

    What external services does it call? What calls it? Whose account is each dependency in? Which of the inbound interfaces have somebody relying on them?

    Outbound flows nobody thinks about — a nightly file, an analytics integration, a partner feed — live here.

  4. Follow the data

    Where does customer data live, how does it get there, and where does it go? This is where regulatory obligation, business risk and the parts you must not break all overlap.

  5. Write down what could not be established

    Explicitly, with priorities and reasons. This list is the actual output of the exercise; the map is what makes it credible.

    A confident picture with no gaps in it has either come from a complete analysis or has turned its gaps into silence, and from the outside those look the same.

  6. Read selectively, guided by the gaps

    Now open the code — specifically the parts the list points at. Reading with a question is an order of magnitude more efficient than reading to build a picture.

  7. Work the list down, then start changing things

    Each answered question is a piece of control recovered. Structural change after this point is ordinary engineering rather than an act of faith.

#Reading code, when you get to it

By pass six you have specific questions, which changes the technique entirely. Four things that work:

  • Follow one request end to end. Pick the most commercially important path — a payment, a signup, an order — and trace it from entry point to database. One path teaches more than fifty files.
  • Read the tests before the implementation, where tests exist. They describe intent, and where they disagree with the code they mark something interesting.
  • Read the history of the risky file, not just its current state. Why it changed, and how often, is usually more informative than what it says now.
  • Write down each answer where somebody else can find it. An answer that stays in your head has recreated the original problem with a new person in the middle.

#Finding the risky parts

Not all of a legacy system is equally dangerous, and knowing which parts are is most of the value.

Four signals, all visible from a structural picture rather than from reading:

  • Concentration. One component many others depend on. Changing it is expensive; being unable to change it is worse.
  • Clustered unknowns. Six open questions about one subsystem is a different situation from six spread evenly. A cluster marks the part nobody ever understood.
  • An external edge with no confirmed behaviour. An outbound integration whose retry, timeout and failure handling could not be established is a live business risk rather than a technical debt item.
  • Declared disagreeing with observed. Where a comment, README or design note says one thing and the code does another, somebody's mental model is already wrong.

#What to stop doing

Cleaning up as you go.

Instead Change nothing until the picture exists. Tidying code you do not understand is how a working system stops working, and the change will not be the suspect because it “did not do anything”.

Reading file by file hoping for a picture to emerge.

Instead Derive the structure first and read selectively. A month of reading produces a worse map than one analysis, in a place nobody else can reach.

Commissioning a documentation project.

Instead Documentation projects on legacy systems almost never finish, because they are unbounded and nobody can tell how much is left. A derived picture is bounded and completes.

Treating comments and READMEs as facts.

Instead Treat them as declarations worth recording. A README describing the system as it was in 2021 is not neutral — it is actively misleading.

Deciding to rewrite before you can describe it.

Instead Describe it first. The description usually changes the decision and always changes the estimate.

#How long this takes

Rough shape, for a system of moderate size
PassEffort
What runs, and whereHalf a day, mostly in consoles
Inventory, edges, dataHours if derived. Weeks if assembled by hand
Writing down the unknownsAn hour, once the passes above exist
Selective readingDays to weeks, bounded by the list
Working the list downWeeks to months, and it visibly shrinks

The important property is the last column of the last row: it visibly shrinks. The reason legacy work feels hopeless is usually that nobody can tell whether it is progressing. A list that gets shorter fixes that.

#What this method does not do

What this does not do

  • It does not recover why anything was built that way. Nothing can, from code alone.
  • It does not tell you whether to rewrite. It gives you the inventory and the risk list, which are the inputs to that decision.
  • It does not assess code quality or find vulnerabilities.
  • It does not establish runtime behaviour. Nothing is executed, so load, latency and what a specific user sees are outside it.
  • It cannot see what is configured only outside the repository — that surfaces as an open question rather than as a component.

Passes two to five, in one instruction.

Somebody with the code runs a read-only analysis; you get the inventory, the dependencies, the data flows and the explicit list of what could not be established. The first one is free.

Build your project map — free See a finished example