Chapter 03 of 12Advanced14 min readOpen access

Reading a Codebase You Did Not Write

Finding the domain logic, and the decisions nobody wrote down.

Reading a Codebase You Did Not Write is the work of rebuilding a theory that somebody else held and never wrote down. It is the fourth fact from Chapter 2, the one that could not be settled from records. It is also the only part of a rescue where the evidence has a person attached to it, and an expiry date.

Key takeaways

  • The code is not the program. Peter Naur argued in 1985 that the program is a theory held by the people who built it and the text is a lossy record, so the decisions you most need are the ones least likely to be in the source.
  • The urge to rewrite peaks in the first hour, when you know least. Spolsky's explanation is that it is harder to read code than to write it, which makes unread code feel worse than it is. Treat the urge as a fact about you rather than as evidence about the codebase.
  • Read history before state. The hotspot ranking from week one is the reading order, and for each hot file the chronological diffs are more informative than the current version of it.
  • Locate domain logic by following the writes, not the entry point. The schema, its constraints, the scheduled jobs and the outbound integrations tell you what the system does; the router only tells you what it claims.
  • Fill a two-column sheet: what it does, and why it is like that. Blank cells in the second column are the map of what has been lost, and closing them is the actual unit of progress.

Read this beside Chapter 2, which produced the file ranking you are about to read in, and Chapter 8, which is about getting truthful answers out of people who expect to be blamed. Chapter 6 turns what you learn here into a decision about rewriting; this chapter is deliberately upstream of that decision.

The first file you open will be the worst file in the system, because that is what the ranking from week one hands you. Eighteen hundred lines, four responsibilities, and a third of the way down a conditional that makes no sense.

You will want to delete the conditional. Do not delete the conditional.

Behind it is a customer who was invoiced twice, a support escalation, and a decision taken at eleven at night by someone who no longer works here. The conditional is the only surviving record of that event. It is badly written and it is load-bearing, and telling those two apart is most of the skill this chapter is about.

The program you inherited is not the program that was built

Peter Naur published the argument in 1985 and it has aged better than most things written about software since. Programming is not the production of a text. It is the building of a theory of how some part of the world gets handled by a machine. The programmer holding that theory can explain why each part of the program is what it is. The text records the result, not the theory.

His case is worth repeating, because it forecloses the obvious objection. One group built a compiler. A second group, wanting to extend the language, contracted the first for support and received full documentation, annotated program texts, written design discussion, and personal advice. That is more handover than any rescue you will walk into. And still the second group kept proposing changes that ignored facilities already inherent in the structure, implementing them as patches that "effectively destroyed its power and simplicity". Naur's conclusion: "the program text and its documentation has proved insufficient as a carrier of some of the most important design ideas".

Ten years later the same compiler had been modified by further programmers with no contact with the original group. Naur's description of the outcome is the best sentence in the literature about inherited code: "the original powerful structure was still visible, but made entirely ineffective by amorphous additions of many different kinds".

That is the thing you have been handed. Not a bad system. A system whose reasons have been removed and whose shape is still faintly visible under the additions.

Why does everyone who inherits a codebase want to rewrite it?

Because reading is harder than writing, and the difficulty gets misattributed to the code.

Joel Spolsky named this in 2000 and the mechanism has not changed. "It's harder to read code than to write it." Reading someone else's code gives you no visible output, no sense of progress, and steady low-grade confusion that feels exactly like encountering bad work. Writing gives you the opposite on all three counts. So the instinct to start again fires hardest at the moment your knowledge is lowest, and it arrives dressed as a technical judgement.

His second point is the one that costs money. Old code looks worse than it is because it has absorbed years of fixes for edge cases found in production. Those fixes are invisible. They look like clutter, they carry no comments, and their justification lives in an incident nobody logged. Throw the file away and you throw away every correction in it, to be rediscovered by your customers one at a time, in order of how much each one hurts.

I hold a rule about this and I hold it against myself. You are not permitted a rewrite opinion until the why column, defined later in this chapter, is at least half filled. Before that point any opinion you hold is a report on your own discomfort.

Read the history before the state

The reading order is already decided. Week one ranked files by change frequency crossed with size, the method Tornhill and Borg used across 39 production codebases, mining version control and issue data rather than inspecting source. The top of that ranking is where cost lives. It is also where the domain logic lives, for the same reason the files are hot: the business kept needing them to be different.

For each of those files, read the commits before you read the file. Chronologically, oldest to newest, skimming the diffs. It takes an afternoon per file and it gives you something the current version cannot: the sequence in which the complexity arrived.

What you are looking for is the commit where the shape changed. Most hot files have one. For six months the file has one job and reads cleanly, and then a single commit introduces a second concept into it, usually under pressure, usually with a message that names a customer or a date. Everything confusing about the file's present state descends from that commit. Find it and you have found the decision, which is different from finding the code.

Then read the file. It will make more sense than it did an hour ago, and the conditional you wanted to delete will have a date attached to it.

Find the domain logic by following the writes

The instinct is to start at the entry point, follow a request inward, and build a picture. That wastes a week here. The routing layer describes what the product claims to do, and a stalled delivery is usually one where claims and behaviour have separated.

Start where state is committed instead. The order matters.

The schema first, constraints included. Unique constraints, check constraints, foreign keys and not-null columns are invariants somebody cared enough about to enforce where the application cannot bypass them. A schema is the most honest document in a legacy system, because it costs real effort to lie in it. Two tables representing the same concept tell you a migration was started and abandoned, which is a finding about the roadmap rather than about the database.

Then the scheduled jobs and queue consumers, because unattended code is where business rules hide. Anything running nightly is doing what the synchronous path could not, and the reason it could not is usually a rule.

Then the outbound edges: payment calls, emails, webhooks, anything reaching a third party or a customer's inbox. Those actions have consequences outside your system, so they carry the most rules, and they are the ones you must not break while repairing anything else.

Only then walk inward to the code that reaches them. By that point you are not exploring. You are checking a hypothesis, which is several times faster.

Michael Feathers' seam model is the lens for what you find on that walk: the places where behaviour can be altered without editing code in place. Where the system has seams, repair is cheap and testable. Where it has none, every change is surgery. Chapter 5 uses that map to build a test net. Here it is diagnostic, and it turns "unmaintainable" from an adjective into a location.

The why column

The artifact of this chapter is embarrassingly simple. Two columns on one page. Left: what it does. Right: why it is like that.

One row per hot file from the ranking, plus one row for every oddity you could not explain on sight. Filling the left column as you read is easy. The right column has one rule: you may only write a reason you could defend to the person who wrote the code. Not a guess. Not "presumably for performance". If you do not know, the cell stays blank.

Blank cells are the point. They are the visible map of the theory that died, and the only honest measure of where you are. Progress in weeks two and three is counted in cells closed, not files read. When a cell closes, record where the reason came from: a person, a pull request thread, a schema constraint, an incident. Provenance matters later, when someone asks whether the reason is real or inferred.

It also turns Naur's test into a number. He wrote that a program's death "becomes visible when demands for modifications of the program cannot be intelligently answered". The blank cells are those demands. Count them. A page 80% blank after three weeks of honest work is not a report on your ability. It is the finding.

The people who still hold the theory, and the weeks you have to ask them

Naur is unambiguous about how a theory gets transferred, and it is not by reading. "For a new programmer to come to possess an existing theory of a program it is insufficient that he or she has the opportunity to become familiar with the program text and other documentation. What is required is that the new programmer has the opportunity to work in close contact with the programmers who already possess the theory."

He goes further, and the phrasing turns a courtesy into a scheduling problem: reestablishing the theory of a program "merely from the documentation, is strictly impossible".

So the interviews are the mechanism, and they carry the deadline you found in week one when you asked who leaves next. Notice periods, contract end dates and goodwill all expire. Each one is a channel closing on evidence that exists nowhere else.

Three rules for those conversations, with Chapter 8 covering the politics properly. Never ask how the system works; you will get an architecture lecture you could have read. Ask why a specific thing is the way it is, with the diff open, citing their own commit by date. Then ask what they would have fixed with two more weeks. That is the highest-yield question in a rescue, because it extracts a private list of known defects without asking anyone to confess to one. Then write the answer into the why column while they are still in the room, and read it back. Half of what you thought you heard will be wrong, and they will correct it for free.

When Naur says burn it

Honesty requires the other half of Naur's argument, which is not convenient for a book about repair.

Having established that revival from documentation is impossible, Naur draws a conclusion most people quoting him leave out. "In preference to program revival, the Theory Building View suggests, the existing program text should be discarded and the new-formed programmer team should be given the opportunity to solve the given problem afresh. Such a procedure is more likely to produce a viable program than program revival, and at no higher, and possibly lower, cost."

He is arguing for the rewrite. Anyone who has spent three weeks filling in blank cells will recognise why: building a theory to fit an existing text is, in his words, difficult, frustrating and time consuming.

Take it seriously, then notice the condition attached. Naur is describing a program whose theory is fully dissolved, where nobody remains to ask and the text carries no recoverable reasons. That state is real, and in it his conclusion is probably right. It is not the common case. The common case is a program with two people left who still hold a third of the theory, a schema full of enforced invariants, and several hundred production fixes encoded as ugly conditionals.

Which gives the rule this book keeps returning to, and which Chapter 6 turns into a threshold you can test. A rewrite is only defensible when you know the requirements better than you know the code.

The trap is the direction of the error. On day one you know neither, and it feels like you know the requirements, because requirements are what the business tells you in a meeting and code is what you have not read. Three weeks of the why column is how you find out which you actually know. Usually the answer moves toward the code, and the case for a rewrite weakens as the evidence improves. When it does not, you hold the strongest possible ground for the most expensive recommendation in software, and you got there by trying to avoid it.

Chapter summary

The program you inherited is not the program that was built, because the program was a theory and the text is a lossy record of it. Naur's compiler case shows that full documentation plus personal advice still failed to carry the design ideas, and that a decade of modification by people without the theory left a powerful structure visible but ineffective. The rewrite instinct arrives before the evidence, because reading is harder than writing and the difficulty gets blamed on the code, and because old code hides years of production fixes that look like clutter. So you read history before state, treating the hot-file ranking as a reading order and the chronological diffs as the real document, hunting the commit where each file's shape changed. You locate domain logic from the writes inward: schema and constraints, then scheduled jobs and queue consumers, then the outbound edges, then the code that reaches them. You keep a two-column sheet of what it does and why it is like that, refuse to fill the right column with guesses, and count progress in cells closed with provenance recorded. And you spend the outgoing team's notice periods asking why about specific commits rather than how about the system, because a theory transfers through contact and not through documents. When the theory really is dead and the requirements really are better known than the code, a rewrite is defensible. Reaching that honestly takes about three weeks, which is why almost nobody who reaches it on day one is right.

You now know enough about this system to change something in it without making it worse, which is a threshold you crossed only just now. The next chapter is about what you change first, and it is not a feature. Chapter 4 is Stability Before Features: how to stop the bleeding while the business is still asking for the roadmap it was promised, and why the pressure to keep shipping through an unstable system is the thing that turned a repairable delivery into this one.

Sources

  1. Programming as Theory Building (1985), reprinted as Appendix B of Agile Software Development: The Cooperative GamePeter Naur, in Alistair Cockburn (Addison-Wesley) · 1985 · Research paper · verified
  2. Things You Should Never Do, Part IJoel Spolsky, Joel on Software · 2000-04-06 · Vendor engineering · verified
  3. Code Red: The Business Impact of Code Quality. A Quantitative Study of 39 Proprietary Production CodebasesarXiv · 2022-03-08 · Research paper · verified
  4. Working Effectively with Legacy CodeMichael Feathers, Pearson · 2004 · Research paper · reported
  5. Lessons from 6 software rewrite storiesHerb Caudill · 2019-02-19 · Vendor engineering · reported