Death by a thousand reasonable decisions

25 Aug, 2026 · 11 min read
Contents

When I read consistent, cohesive code, I have a feel for its author. I may disagree with the ideas, but I can feel where the author is driving it. Other times the feel is split-brain — the code’s “energy” is fractured, the work of unaligned hands each solving its own small problem, none with an idea where the whole is going. Immediately I feel it should be rewritten or refactored holistically.

Here is the paradox. Most of us are dedicated professionals with years of experience, and bad code is everywhere. Programmers I respect produce it; my own code is far from perfect. Why?

Death by a thousand reasonable decisions

The answer I have settled on: nobody writes bad code on purpose. It accretes. Every one of the thousand decisions behind it was reasonable at the moment it was made, by the person who made it, with the context they had. Bad code is emergent, and the mess is, in the end, a scalability problem we inflict on ourselves. This post is about how the accretion happens and what to do about it.

Two minds

The same code is worked by different people at different times, and they may hold different ideas on how this particular piece should develop. They fix and augment it along different philosophies, and the result is inconsistent. It starts small and grows over time, until it is too late to correct without major refactoring. And frequently these “different people” are you at different times of your career — we change, and our preferences and philosophies change with us.

Fred Brooks called the thing that gets lost conceptual integrity : a system that reflects one set of design ideas, whether from one mind or from a few minds in agreement. The “feel for the author” is conceptual integrity from the reader’s side. While it is there, the code can be argued with. Once it is gone, the code can only be patched.

Passing through

Some of the different minds never meant to stay. Temporary consultants, programmers “borrowed” from other projects, anyone whose job is to deliver and move on. Running and maintaining the thing is not their job, so they favor the answer nearest to hand: the literal one. “You want search? Let’s install a database cluster for that! Let’s pull in some library for that — an unknown one, or a well-known one that is a poor fit!” — and other such answers to challenges. They have no time to think about how it will be used, and by the time it matters they will be far away. Every one of those answers is defensible on the day it is given; the bill arrives after they have left. This is the same shape as the agent problem below, with a social cause instead of a technical one.

Every session a stranger

The most extreme version is already here: an AI agent. Every session is a different author with different ideas. Ask “what about X?” and the answer is “that wasn’t in this session, I checked!” And bad decisions get enshrined: yesterday an agent added something we wouldn’t have, but it’s fine, we’ll remove it later. Later a programmer notices the problem, and an agent works around it, because “that’s how you wanted it.”

This is not hypothetical; engineers already report it. One account of building with an agent describes output where each function works on its own, yet the system reads like ten different consultants each wrote a few functions . A reply in the same thread , from systems and C++ work, reports the same: correct functions that still add up to “a codebase that feels like strangers wrote each file — broken abstractions, happy-path tests, no shared story.” Every piece is sound; the whole is not. Alignment was always needed. With agents it is needed every morning.

No time to think

Usually bad code is the product of rushed decisions, made by programmers who were, themselves, rushed. The rush shows up in two ways, and it is selected for before the first line is written.

The first is fixing at the wrong level. Programmers rarely have enough time to learn a codebase’s intricacies, so they pick the wrong place to fix. That number should be green, yet it is red? Restyle it, without tracing why it went red. Numbers arrive in different currencies? Patch it at the presentation layer. Each fix is quick, and each is made by someone who could not afford to find the real seam.

The second is what replaces thinking when there is no time for it: “best practices,” “patterns,” “a million lemmings cannot be wrong,” “nobody got fired for choosing” — insert the name of any large vendor — and the rest of the industrial incantations. Some of these are good practices. None of them is a substitute for thinking, and the best of them were written by people who had thought about their problem, which is rarely ours. That is how we get so many overengineered yet badly executed projects: all the machinery is there, and the judgment that would have made it fit is exactly what there was no time for. It is also where industrial solutions come from, and why there are so many of them.

Hiring selects for this, and so it propagates. The usual practice favors fast pattern-matching over thinking. In an interview a technical problem gets ten, fifteen, twenty minutes. “But these problems are simple!” — then that is what we are hiring for? Are all our projects a collection of ten-minute problems? “We just want to see how the person thinks!” — yet the offer goes to whoever delivered a working solution in those minutes.

Small innocent changes

Small changes compound. Every change (a steady stream of feature and bug tickets) makes sense on its own and is perfectly sound in isolation. Yet together, over time, they grow into a hairy beast nobody wants to own.

A story circulates that I read once and have not been able to find again, so take it as a parable. A programmer turned entrepreneur built a barbecue-sauce brand on his grandmother’s recipe, sold it to a large company, and negotiated the final word on any change to the recipe. The company’s food people set out to cut costs one ingredient at a time. Each proposed change was prepared, he tasted it against the current sauce, and it was fine. All the approved changes together were terrible. Every step passed the quality gate, because every step was judged against the previous step and never against the original. That is the mechanism exactly: pairwise indistinguishable, cumulatively unrecognizable, and nobody at the table ever decided to ruin the sauce. Code review has the same gate: every diff is judged against the last commit, never against the design.

The problem

On its own, each inconsistency is too small to count as a problem — “not important,” “we can do it either way,” “small fry.” The trouble is that they accumulate. Small inefficiencies add up to a needlessly slow response time, and response time is a client-facing feature now. “I will apply the Pareto rule and fix two or three things for a big win” — try that against thousands of small bad decisions made over years.

I have watched teams make harmless decisions, each fine in isolation and green in its tests, that did not work together for performance reasons. The work had to be done twice. The comfortable version of the Pareto rule says we will optimize the one percent and it will buy us ninety-nine percent of the speed. In a long-lived project the one percent has usually been found already: no peak left, only a long tail, and the tail is the thousand “inconsequential” decisions. It is rare that one thing can be optimized and everything flies; there are dozens, sometimes hundreds, of operations that could be sped up, and none of them is the peak. That is the flip side of the warning about premature optimization: the decisions that were each too small to bother optimizing are exactly the ones that add up.

It is the same in every other dimension. A file that is hard to read and understand? Guess where the next problems cluster. In the end it is all one thing: a scalability problem, and the part that stops scaling is the people.

Machines scale by adding machines. Coherence does not scale by adding hands; every workshop price list knows it (so much per hour, more if you watch, more still if you help). One mind keeps a small codebase consistent for free; add contributors, add years, and now add agents, and the number of decisions grows faster than any one head can hold. The artificial heads are no exception: an agent’s context is finite too. The ceilings may be higher, but they are ceilings. Past that point everyone works from a fragment, and we drown in it: a company can absorb a handful of defects, but not thousands, and many of those are self-inflicted, the residue of well-meaning people not keeping to a few shared conventions.

Ownership

So assign owners. I like a part of the codebase to be owned by a few people with a tie-breaker, or by a single owner, with changes routed through them. Often this just means “different projects” with team leads. Somebody has to be responsible for it.

If it’s everybody’s responsibility, it means nobody is responsible. If one person is responsible for everything, again nobody is responsible. This is no way to run a project. The workable arrangement is the middle: a few aligned minds per part, and a name to ask. Collective ownership works while the team is small enough to be one mind; it stops working exactly when it stops being one.

Managers often dislike code ownership: fewer chances to put any person on any ticket (programmers should consult the code owners first), and “what if that person quits tomorrow?” These are legitimate concerns, and they should be dealt with, not ignored. The first is the price of the mechanism: consult-first is how coherence survives contact with a new ticket. The second is remedied the ordinary way: assign a deputy, a second person, an understudy. Conway’s law says the code will mirror the communication structure regardless; ownership is choosing that structure on purpose.

What to do

Make “should we refactor?” a periodic, scheduled question. Fold a review and refactor of the existing code into every major feature. I have long scheduled work tagged “technical debt” — cut corners, simple-but-slow implementations, use cases we missed. Managers were usually alarmed: “we just started the project — how do we have technical debt already?” It is a matter of velocity, not of time. Move fast and we cut corners or take the easy path; put it in front of users, and the case we thought was rare turns out to be all the time. Cutting the corner was often the right call, and a skilled one (see Cheating as a programming discipline ); the discipline part is remembering to come back.

Some of that debt is not even a shortcut. Programming almost always turns up sharp corners as it goes: unforeseen problems, an architectural decision that makes some things unnecessarily hard. If they are not fixed when found, they go into the debt pile in the best case. This happens to good designers and experienced programmers alike; it is simply how building things goes, and it should be planned for. Budget time for the unforeseen, and the manager’s question answers itself.

Lehman’s second law puts it plainly: complexity grows as a system evolves unless work is done to reduce it. Refactoring is that work; the schedule is what makes sure it happens. And it is affordable, because the artifact is the cheap part (see Code is cheap ); the expensive part is the thousand decisions we would otherwise keep paying for.

And make the alignment explicit. The conventions, the direction a module is headed, the decisions already made — if they live only in one head, the next author (human or AI) cannot follow them. Write them where the next author will look first: a conventions file at the root, a decision log next to the code. Schedule it, write it down, do it — doubly so with agents, where every session starts from a blank slate.

None of this is a call to refactor everything: some inconsistency is genuinely harmless, and chasing it is its own thousand reasonable decisions. The skill is telling the two apart, and it belongs to whoever owns the code.

Summary

Nobody authored the mess. It accreted from reasonable local decisions made under incomplete context and in a hurry, by different authors, by people passing through, or by the same person at different times, with nothing holding the whole together. The fix is unglamorous: give each part an owner who keeps it coherent, and pay the complexity tax on purpose, on a schedule, before it compounds. Do that, and the code gets its author back — someone a reader can disagree with, and follow.