Chapter 03

The Four Qualities of Durable Code

Over the years I’ve settled on four qualities of durable code: it is useful, maintainable, debuggable, and evolvable. None of them is a box you tick and finish. Each names a direction you keep working in, and a system holds to it or slips from it gradually as it changes. The four together are what I mean by durable code.

Useful

The way I’ve come to see it, software is useful only when it serves a real purpose for a real entity, whether that’s a person with a job to do or another system calling my code. Three things have to hold for that, and in my experience none of them shows up in the code itself.

It serves a real purpose for a real entity. The purpose has to be one the entity already holds, before I build anything for it. I’ve watched a team ship a clean bulk-export feature because export sounded useful on its face, then learn that the customers who asked for it only wanted to mail one monthly report to their accountant. The feature worked, the code was clean, and the usage logs stayed flat. When I’ve seen software miss useful, it usually missed right here, on a purpose I assumed instead of confirmed.

The right combination is a judgment call. Useful software is usually the right combination of pieces that already exist. Wheels are five thousand years old and suitcases are over a century old, but the rolling upright with a telescoping handle didn’t arrive until 1987. Every piece sat on the shelf for decades while people lugged heavy bags through airports. What was missing was the judgment that wheels were the work worth doing, ahead of a better latch or a lighter shell or any of the other places a maker could have spent the year. The same judgment tells me to skip effort that won’t pay off: tuning a service for a hundred times the load it will ever see, or polishing an animation before the flow underneath it works, spends real effort on a part the purpose never needed. So I try to choose the combination that serves the purpose, and to say no to the work that only looks like quality.

The signal comes from outside the code. I can’t read the right combination off the code, or off the spec. The same pieces that produced a navigation app people rely on also produced a hundred location apps nobody remembers; the parts were the same, and what set them apart was the fit to a purpose someone had. The clearest place that fit shows up is in what the entity does once the software is in front of it. So I ship, I watch, and the usage tells me what the design couldn’t. Until then, every judgment I made along the way is a bet.

Useful is a judgment you make from the inside, and the world confirms from the outside.

AI makes code cheap, and a wrong guess built cheaply is still a wrong guess. I find the new speed is easy to pour into more features, more polish, more tuning, and the day feels productive while I do it. None of that moves usefulness, and the world hasn’t gotten any faster at telling me whether the combination fit. So I can now build faster than I can learn whether what I built counted, and most of the extra speed goes into work the purpose never needed. That pattern is the Progress Mirage.

Maintainable

Code gets changed far more often than it gets written. A bug turns up, a requirement shifts, a dependency moves, and someone opens the file again while the system is live. Maintainability is what that change costs them. These days that someone is as often an AI agent as a person. Three things keep the cost down.

The next reader can understand it. Most of the cost of a change is understanding the code before you touch it. The reader doing that is rarely the author. It might be a teammate, a new hire, an AI agent, or you once you have forgotten the details. None of them can change code they do not understand. Clear structure lets a reader follow it. Here is a test. Give the file to a reader who did not write it, person or agent, and ask what the code does and where a change would go. If the answer is a guess, the code is not legible yet.

The next change is cheap and safe. Once you understand the code, the change should be small, local, and low-risk. Maintainable code keeps the cost of a change low. Unmaintainable code raises that cost without warning. Add one field to a form, and you find it wired into three other systems through conventions nobody wrote down. The quick task becomes a week. Teams feel this before they measure it. It shows up as the estimate that blows up and the file everyone routes around. An AI agent walks into the same trap. Pointed at a tangled module, it makes a confident change that compiles, passes the tests it can see, and breaks an assumption two systems over.

Understanding does not live in one head. The sharpest failure is the module only one person can safely change. Their name comes up whenever that area changes. If they leave, the capability goes with them. The AI version is newer and quieter. An agent writes the code in one session and never writes down its reasoning. Close the session, and the only record of why the code looks this way is the code itself. Maintainable code does not depend on one person staying, or on one context window staying open. The understanding lives in the code and the notes around it, where anyone or anything can pick it up.

A clean-looking file is not automatically a maintainable one. The cursed module might be neatly formatted and pass every lint check. Tidy formatting is a proxy for maintainability, and a weak one. What matters is whether the next reader, person or agent, can change the code without paying a tax nobody budgeted for.

Hand someone the code without the reasoning, and you’ve handed them a guess.

AI puts pressure here because code now arrives faster than the understanding behind it. Generated code gets hard to maintain when the person who approved it only reviewed it. They never built the understanding an author has. What lands in the repo is the code, without the reasoning that produced it. Maintainable code keeps that reasoning recoverable, so the next person or agent can find it. That is what keeps a system durable as the hands on it turn over. When the reasoning is lost, the result is the Change Spiral and Missing Reasons.

Debuggable

Every running system eventually does something it should not. A request that worked yesterday starts returning a 500, a report shows the wrong total, a scheduled job stops halfway through. When that happens, someone has to find out what went wrong and put it right. Debuggability is how much trouble that is, and it comes down to whether the system lets you work out the cause from the inside. The person doing the work might be an engineer who knows the system, or an AI agent handed the incident, and either way what they can do depends on what the system recorded as it ran and how it is put together. Three things tend to decide how hard the job is.

Failure leaves evidence. To reconstruct what happened, you need the system to have written down enough about what it was doing while it ran. The most basic form of this is the stack trace. When an exception is raised, the runtime hands you the chain of calls that led to it, and that chain stays available only as long as something holds on to the exception. So the way code handles its exceptions decides whether the evidence survives. The tempting move, when a call might fail, is to wrap it in a catch, log a short message, and carry on; that keeps the program running, but it throws the chain away, and the easiest explanation of the failure goes with it. The alternative is to catch the exception, add a note about what the code was attempting, and re-raise it, which keeps the chain intact and lets it travel up with the failure. The difference is about a line of code, and it only pays off later, when something has broken and you are trying to understand it. An agent asked to investigate starts where you would, so if the trace was discarded, it has nothing solid to work from and falls back on guessing.

The evidence points to the cause. Recording that something failed only helps if the record tells you where to look, and a lot of error handling falls short here. A validation error that comes back as “invalid input” tells you that something about the input was wrong and leaves you to work out which field and which rule by yourself. The same failure is far more useful when the message says it expected an ISO-8601 timestamp and received “tomorrow” in the start_date field, because then you know where to go. The structure of the code does the same thing at a larger scale. When the parsing, the business logic, and the database access each live in their own place, a failure tends to announce which one it came from. Mix those concerns together in the same functions, and a single symptom could have started anywhere, leaving you to read the whole path to find out.

You can reproduce it in isolation. Often the quickest way to understand a failure is to take the code that produced it, run it on its own with the input production gave it, and watch what it does. Whether you can do that comes down to how the code gets hold of the things it depends on. Code that builds its own database connection internally, or reaches out to global state, can really only run inside the full system, so the best you can manage is to reproduce the problem by approximation, which sometimes works and often does not. When the dependencies are passed in from outside, you can swap them for simple stand-ins and run the suspect code by itself, and that is usually what turns a long investigation into a short one.

None of this depends on new technology. Stack traces, error messages that say something specific, separated layers, and code that can run on its own have been understood for decades. The reason some systems have these properties and others do not is mostly a matter of history. A team adds them one at a time, after the incidents that show why they were needed: a message gets rewritten because a bug took too long to find, a test gets added because a release broke something quietly. Over time those additions accumulate into a system that explains itself when it fails.

Debuggability is the residue of past incidents.

This matters more now because AI changes the cost of the small moments where those properties are added or skipped. Writing the catch block, the error message, and the logging takes an agent seconds, and it does that work without having been through the incidents that teach a person why the details matter. So the code runs, and when it fails it tends to fail quietly, because nothing at the time pushed it to explain itself. A system built that way breaks without telling you why, which is the pattern this book calls Invisible Decisions, along with part of Missing Reasons.

Evolvable

Every product that survives gets bigger, picking up features and screens and settings as it goes. Most of them slowly turn into junk drawers, each new thing bolted on wherever there was room. A few keep their shape, so a new feature still feels like it belongs beside the original ones. Evolvability is the quality that lets them do it. Three things hold that shape.

A small core that generates features. Systems that grow well are built on a primitive: a small idea so general that the features turn out to be arrangements of it. Notion is the clear example. Instead of a notes feature, a tasks feature, and a database feature, it shipped one thing: the block. A paragraph is a block, a heading is a block, a database row is a block, and a page is a block made of blocks. The kanban board, the calendar, and the timeline came later, and each was a new arrangement of blocks rather than a new kind of thing. The core is the room; the features are the furniture.

New things land where people expect them. Growth has to confirm the model people already carry, or it reads as clutter. Most of that is steadiness in two places. The first is where things live: once navigation is on the left, content in the middle, and settings on the right, the arrangement holds, and a new feature inherits a home without anyone choosing one. The second is what things are called. If the system already has Pipelines and Triggers and Steps, the next concept should sound like a member of that family, so a user can half-guess what it does before reading the docs. There is a quick test for a system you suspect has lost its shape: ask a designer, an engineer, and a product manager to each sketch, alone, where a new feature would go. If the three sketches match, the system has a shared model to grow into; if they do not, the next feature lands wherever the loudest voice in the room wins.

Someone guards the model. None of this holds on its own. Every system that lives long enough faces pressure to add the feature that does not fit. Keeping the shape means someone has the authority to say no, or to rework the model so the new thing belongs. Fred Brooks called this conceptual integrity and argued, half a century ago, that it was the most important consideration in system design. There is no formula for it. The people who chose the block for Notion, or “everything is a file” for Unix, had taste, and you only learn the taste was right by what the system later takes on without strain.

Nobody decides to build a system nobody can predict; it happens one unplaced feature at a time.

This is where AI presses hardest. An agent handed a local task produces something that looks right in the file it lands in, and it has no way to hold the shape of the whole or to weigh the next ten features against a model in someone’s head. Generating code that ignores the shape has never been cheaper, and living with that code costs what it always did. Left unguarded, a codebase worked by agents loses its shape faster than its authors expect. That is Shape Drift.

The four qualities together are what durable code means.