Designing for the engineer who inherits this in 2029
Legibility is an architectural property, set by where the boundaries go, what things are called and what gets deleted. It cannot be added at the end by writing a wiki page.
Engineering practice9 min read
We have been on some systems long enough that we are now the people inheriting them. The engineer who takes a call about a failed batch job at two in the morning did not write it, has never met the person who did, and in several cases had not joined the firm when it shipped. That engineer is the reader we design for. Everything we mean by legibility is an attempt to make their next forty minutes go well.
Legibility is an architectural property. It is set by the same decisions that set availability and running cost: where the boundaries go, what things are called, what gets deleted and when. It cannot be retrofitted by a technical writer in the final sprint, and a style guide does not produce it. A system is legible when a competent stranger can build a correct enough mental model of it, fast enough to act, without finding anyone who was there.
The reader is not a beginner
A common mistake is to write for a junior engineer in their first week. That reader does not exist in the situations that matter. The person who inherits a system in 2029 will be experienced, will be under some time pressure, and will hold a partial and slightly wrong model of how the system works, assembled from a dashboard, a ticket and whatever they skimmed on the way in. They do not need the language explained. They need to find the right file, understand the constraint that shaped it, and know what happens downstream if they change it.
Design for that reader and most of the standard advice reorders itself. Consistency beats elegance, because a stranger navigates by pattern and every exception costs them a lookup. Explicit beats clever, because clever code requires reconstructing the author's reasoning before you can safely touch it. Ordinary names beat precise but novel ones, because a stranger searches the repository with the words they already have in their head.
Boundaries outlive everything inside them
Internals get rewritten. Over a decade a service will change language, framework, database and hosting, and none of that reaches anyone outside it. Boundaries behave differently. An API contract, the ownership of a table, the name of a queue topic: these end up embedded in other teams' code, in finance's reports, in a vendor integration, and eventually in the way the client's own staff describe their work. Changing a boundary requires coordination across organisations, which is why boundaries mostly do not change. They are the part of the design that has to be right for ten years.
So draw them around business capabilities rather than around the current organisation chart or the current technology. A boundary drawn around the team that handles dealer pricing stops making sense the week that team is merged into commercial operations. A boundary drawn around pricing itself survives the merge, and the two front ends that come and go afterwards.
Keep the count low. Every boundary is a place where a reader has to stop, find a contract, switch repository and rebuild context. We have twice built systems where the number of services was chosen for organisational convenience rather than for any property of the domain. The code inside each service was perfectly good. The system was unreadable, because nobody could hold the call graph in their head, so every change carried an outage risk unrelated to the change itself. That one is ours to own, and it took years to unwind.
Naming is a durable interface
A name propagates further than its author expects. It goes into the schema, the log lines, the dashboard titles, the alert text and the ticket queue. Within a year or two it is in the vocabulary of the client's operations staff, who will say it out loud in meetings we are not in. Renaming a concept in code is an afternoon. Renaming it in an organisation takes a year and usually does not finish, so half the estate uses the old word and half the new one, and the reader in 2029 learns both.
Name after the domain rather than the mechanism. A table called dealer_price_override tells a stranger what it holds. A table called pricing_aux_2 tells them the author was in a hurry. The expensive cases are fields whose meaning accreted: a status column that started with three values, carries eleven today, and where four of those only make sense in combination with a flag stored somewhere else. Nobody decided that. It happened one reasonable change at a time, and it is now a permanent tax on every reader.
Numbered suffixes are the clearest visible sign of a naming decision that was deferred and never made. Anything ending in 2, anything called new, a ProcessorV3 sitting next to a ProcessorV2 that is still handling live traffic. Each of those is a small unpaid debt, and the person who eventually pays it is not the person who took it out.
Dead code is worse than missing code
Dead code consumes reader attention at exactly the moment attention is scarcest. During an incident the first question about any code path is whether it can execute at all. If a third of the repository is unreachable, every question takes twice as long to answer. Missing code at least asks nothing of the reader.
Version control is the archive. The branch, the tag and the commit history exist precisely so that nothing has to be kept around in case it is needed. Feature flags that shipped two years ago and have been permanently on ever since are dead code with an extra layer of indirection. So is the configuration option nobody has ever set, the second implementation retained in case the first one disappoints, and the commented-out block with a name and a date beside it.
The habit worth enforcing is small: whoever removes the last caller removes the callee in the same change. It costs nothing at that moment, because the person doing it has the whole picture in their head. Six months later it costs a day of proving that a function nobody recognises really is unused, and the usual outcome of that day is that the function stays.
The comment that earns its place
Comments that restate what the code does rot within two refactors, and they rot invisibly, because nothing fails when they become false. Comments that record a constraint survive, because the constraint survives. That is the whole rule, and it is worth applying literally.
The kinds that pay for themselves: why the obvious approach was rejected, what external fact forces this shape (a retention period a regulator sets, a vendor API that throttles at a specific rate, a hardware clock that drifts in the heat), what breaks elsewhere if this is changed, and where the edge of a deliberate workaround lies. Each is information the code cannot express, because code records the decision and never the alternatives killed on the way to it.
A comment in our own website's content file explains that a per-logo scale value is an optical correction rather than a size, and gives the limit above which the image goes soft on a high-density display. Nothing in the code says that. Without it the next person adjusts the number by eye, ships a blurry client logo, and has no way of knowing why. It is a boring comment about a trivial file, and it will still be correct in 2029.
The test we give people: if the comment would still be true after the function beneath it is rewritten from scratch, write it. Otherwise the code should be saying it.
The two areas strangers always fail
Watch enough engineers pick up an unfamiliar system and the same two walls appear: getting from a clean machine to a running build, and working out where a configuration value in production actually comes from. Both are invisible to the team that has been doing them daily for three years, which is exactly why both go undocumented.
The fixes are cheap during the build and expensive afterwards. Have the pipeline execute the setup instructions, so a change that breaks them fails a build rather than surprising someone in a year. Make configuration resolution traceable at runtime, with a startup log line stating, for every value in effect, where it was read from.
Legibility has a price, and it is worth naming
Some of this costs performance, some costs lines of code, and some costs the private pleasure of writing something neat. Explicit code is longer. Repeating a small piece of logic rather than abstracting over two barely related cases means two things to change instead of one, and reviewers will say so.
We take that trade deliberately in most of the codebase. A wrong abstraction is more expensive than duplication, because a stranger has to understand the abstraction, work out which of its behaviours apply to their case, and then discover that a third caller depends on an accident of the implementation. Duplication is visible and stupid, and that is its virtue: the reader can see both copies and decide. Where we do not take the trade is in hot paths and anywhere a security boundary is being enforced, because there the cost is not paid in reading time.
Testing for legibility before the team leaves
Legibility can be measured, roughly, and roughly is enough. Take an engineer who has never worked on the system, give them a real change to make, the repository and whatever documentation exists, and no access to the original team. Time how long it takes them to find where the change belongs. Not to make it correctly, just to locate it.
If a new engineer cannot work out where a change belongs, the problem is the architecture, not the engineer.
The enforcement mechanism is rotation. We move engineers onto systems they did not build while the original team is still reachable, because that is when the parts that only work because somebody remembers something become visible. It is the cheapest handover rehearsal available, and it happens years before anyone would otherwise think about handover.
A second measure costs nothing: read an hour of the system's production logs as though arriving cold. If they do not make it clear what the system was doing and whether it was healthy, the engineer at two in the morning will get nothing from them either. Logs are read almost exclusively by strangers, which makes them the one part of a codebase where writing for a stranger is simply the obvious thing to do.
The self-interest in it
We staff for year three from the first week, and the people who build a system are the people who answer the phone about it later. The engineer who inherits this in 2029 has a reasonable chance of being one of us, working out why a colleague who has since moved on made a decision that looks strange now.
That is the honest reason we bother. Writing for a stranger turns out to be the same activity as writing for yourself once you have forgotten, and everyone forgets on roughly the same schedule. The difference between a system that is still maintainable after a decade and one that is quietly replaced at enormous cost is rarely a matter of technology choice. It is whether the people who built it expected to be read.
More on engineering practice
All writingNovember 2025
What fifteen years of maintenance taught us about design
Fifteen years in, the systems we still maintain have taught us more than the ones we launched. Some early decisions aged quietly well. Others cost us for a decade.
August 2025
Senior engineers in a client's team, and the reporting line
Embedded engineering works when one question is settled in writing: who sets priority. Ambiguity there produces most of the friction people blame on culture or communication.
May 2024
Offline-first is a product decision
Queueing writes is the easy half. What happens when a representative returns from two days without signal is a product question, and it usually gets answered by whoever wrote the sync code.
Talk to our engineering team
Tell us what you need built, modernised or maintained. We will tell you whether we are the right firm for it and what it costs.