3B Philosophy Software complexity is a slow leak. It never announces itself. It builds up in wrappers around wrappers, in dependencies that drag in more dependencies, in abstractions I added for futures that never arrived. By the time you notice, the codebase is underwater and nobody fully understands any of it anymore. Usually I'm the one who put it there. These notes are my attempt to push back against that. I've broken every rule below at some point, usually on a deadline or a Tuesday, and then spent the rest of the week paying for it. So read this the way it's meant: not a manifesto from someone who has it all figured out, but the list of mistakes I wish someone had handed me earlier. Simplicity is not optional I used to think simple code was a luxury, something you graduate to once the deadlines calm down. The deadlines never calm down. Simple is the baseline. Code I can't read in one pass is code I don't trust, and code I don't trust is code nobody dares to touch. When complexity is unavoidable, contain it. A dense algorithm belongs in a function whose name says what the algorithm is for, so the rest of the code can stay readable. The point is not to hide the complexity. It is to stop it from spreading. The measure of a codebase is not how much it does. It is how much you have to hold in your head to work on it. Lightweight by default Every line of code is a liability. Every dependency is code you did not write and cannot fully audit. Before adding either, I ask whether I actually need it. I usually know the answer before I finish the question. The standard library exists. A flat array is often enough. A plain loop is often clearer than the iterator chain I was tempted to build instead. Reaching for a library before finding out whether the problem is actually hard is an instinct, and I've learned to fight it, at least long enough to check. When I do take a dependency, I ask what I'm taking it for. One function out of a 40,000-line library is not a good trade. I know because I've made that trade, and I remember the bill. Small libraries are a different story. If something is tiny, does one thing I genuinely do not want to write myself, and I can read the whole source in a sitting, it is probably fine. But I have to actually read it. If the API is a mess, that mess is mine now. A library you cannot use without checking the docs every five minutes is not saving you time. It is renting it to you at interest. Some large dependencies are unavoidable. A well-audited crypto library, a proven compression library, something where being wrong is expensive. That is fine. Before accepting one, though, I look for a smaller alternative covering the same ground, and I prefer it if it is maintained and close enough in quality. If there is genuinely no alternative, I take the big one and stop second-guessing. The mistake is not using a big library when you need it. The mistake is using one when you did not need to. Interfaces are contracts The surface a user or caller touches is the part you can never easily take back. Keep it small. Expose what is needed and nothing else. Internals can change freely. Public APIs, CLI commands, config formats, anything external cannot, not without cost. User-facing things should be obvious. A CLI should read like intent. An API should be guessable. If someone has to dig through documentation to do the straightforward thing, the interface is wrong, whatever the test suite says. Internal code is different. It does not need to be pretty. It needs to be correct, and clear to whoever is working on it. Those are not always the same as tidy. One job is a suggestion, not a law The Unix idea that every tool should do one thing and do it well is useful if you apply it at the right scope. A function should do one thing. A module can do several related things. A program can do what it needs to do to be useful without being split into ten cooperating processes for purity's sake. Composability is good. Splitting a coherent tool into fragments because someone wrote a rule about it is not. If the functionality belongs together, keep it together. The user should not have to wire three programs together to do something that is obviously one task. Deleted code is progress My instinct when something is not working is to add more. A flag, a fallback, a compatibility shim. Usually the right move is to find out why it is broken and fix that. More code on top of broken code is still broken code, just harder to get at now. The same goes for features. A feature nobody uses still has to be maintained, tested, and understood by the next person through the door. Cutting it feels like waste, which is exactly why I keep putting it off. Cut it anyway. The 3B rule When I'm in doubt about a specific decision, three questions cover most of it. Be simple. Can someone understand this without context? If not, is the complexity earning its place? Be convenient. Is the interface guessable? Does it behave the way a person would expect before they read anything? Be lightweight. Is there a shorter path to the same result that does not cost readability? These three pull against each other, and I've stopped pretending they don't. When they fight, clarity beats brevity, and correctness beats both. Naming Names matter where people see them. Anything a caller or user touches — public functions, exported types, CLI flags, config keys — should say exactly what it is. A bad name on a public interface is a lie the code tells everyone who uses it. Internal code is different. A variable inside a fifteen-line function that nobody outside will ever see does not need a dissertation for a name. The person working in that function has context; use it. What matters is that the code in front of you makes sense while you are in it, not that it reads well to a stranger skimming past. That said, do not abuse this. If a better name costs nothing, take it. State State is where bugs live. Every piece of state is something that can be wrong, something that has to stay in sync with something else, something you have to reason about every time you touch the code around it. Less state is almost always better. Sometimes adding state genuinely helps. A flag that removes a whole branch of recalculation, a cache that flattens a loop, a counter that skips a scan. That is fine, as long as I am honest about the trade. I am trading correctness surface for simplicity or speed somewhere else, and I should know what I did and leave it obvious in the code. Correctness and bugs Software should work. That is not a high bar, but it is the bar that matters. Edge cases, bad input, unexpected state: these are not rare. They are the normal operation of anything real people use in real conditions. That said, not every bug is a bug. Sometimes unintended behavior turns out to be useful, and removing it breaks things people depend on. Use judgment. Does the behavior make sense given what the software is supposed to do? If yes, it is a feature now. If no, fix it properly instead of patching around it. A half-working feature is worse than no feature. If something cannot be made reliable, cut it, or hold it until it can be done right. Shipping something broken and calling it a known limitation is just shipping something broken with a friendlier label. Documentation Document things that cannot explain themselves. User-facing code needs documentation, because the user has no other way to know what is going on. Complex internals need it too, because the next person touching that code — possibly me, six months from now, having forgotten everything — will not have the context I have today. Code that follows this philosophy and is not user-facing generally does not need comments. If a function is named well, does one thing, and its logic is straightforward, the code is the documentation. A comment that just restates the code is noise. It goes stale, it gets contradicted by a change nobody updated it for, and it trains people to ignore comments. Write comments for the why, not the what. The code already says what. If the why is not obvious, say it once, clearly, and move on. Be hackable A codebase you cannot get inside of might as well be a binary. Being able to read the code, find the part that matters, and change it without a guide is not a luxury feature. It is what makes the thing yours instead of just something you are stuck maintaining. Navigable means a stranger can find the right file by guessing. Directory names should say what is in them. File names should say what they contain. If finding where a behavior lives requires already knowing the codebase, the structure failed at its one job, which is to tell you where things are. Learnable means the code teaches itself as you read it. You should be able to start at an entry point and follow the thread without a diagram or a wiki page open in another tab. If understanding a module requires reading five other modules first, that is not depth. That is a knot. This pulls the same way as everything else here. Simple code is hackable code, and a flat, obvious structure is easier to get into than a clever one. But it deserves naming on its own, because a codebase can be simple in isolation and still be a maze to move through, if the pieces are scattered, the naming is inconsistent, or the entry points are not obvious. Prefer structure a newcomer can guess over structure that is optimal but needs a map. A slightly less elegant layout that someone can find their way through unassisted beats a more elegant one that needs onboarding. The map is not documentation. If you need the map, the structure lost. This does not mean flattening everything into one file, or refusing to build real abstractions. It means every layer you add should pay for itself in the same currency as a dependency: what does this cost someone trying to get in here for the first time, and is what they get back worth it. On the rest Software does not exist to be admired. It exists to work. Elegant code that ships late and broken is worse than ugly code that ships and works. The goal is software that does what it is supposed to do, can be understood by whoever has to touch it next, and does not collapse under its own weight over time. I have not always written code like that. I have written code that hit none of those marks, on purpose and by accident. So read this as the target, not the track record. If your rules get you to the same place by a different road, keep them. Everything else is negotiable; this doc is included.