Software complexity is a slow leak. It does not announce itself. It builds up in wrappers around wrappers, in dependencies that drag in more dependencies, in abstractions added for futures that never arrive. By the time you notice, the codebase is underwater and nobody fully understands any of it anymore.
The principles here exist to push against that. They are not a checklist. They are a way of thinking about software.
Simple code is not a nice-to-have. It is the baseline. Code that is hard to read is code that is hard to trust, hard to debug, and hard to hand off. Complexity should only exist where the problem demands it, not where the programmer demanded it.
When complexity is unavoidable, contain it. A dense algorithm belongs in a function with a name that describes what it does, 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.
Every line of code is a liability. Every dependency is code you did not write, cannot fully audit, and cannot fully control. Before adding either, ask whether you actually need it.
The standard library exists. A flat array is often enough. A plain loop is often clearer than an iterator. The instinct to reach for a library or framework before attempting the thing yourself is worth fighting, at least long enough to check if the thing is actually hard.
When you do take a dependency, know what you are taking it for. One function from a 40,000 line library is not a good trade.
Small libraries and single-header projects are a different story. If something is tiny, does one thing you genuinely do not want to write yourself, and you can read through the whole source in a sitting, it is probably fine. But read it. If the API is a mess, that mess is now your problem too. A library you cannot use without constantly checking how it works is not saving you any time.
Some large dependencies are unavoidable. A well-audited crypto library, a proven compression library, something in a domain where being wrong has serious consequences. That is fine. Before accepting one though, look for a smaller alternative that covers the same ground. If you find one that is audited, maintained, and close enough in quality, prefer it. If you genuinely cannot, take the large one and stop second-guessing it. The mistake is not using a big library when you need it. The mistake is using one when you did not need to.
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.
Internal things are different. Code that is not user-facing does not need to be pretty. It needs to be correct and it needs to be clear to whoever is working on it. Those are not always the same as being tidy.
The Unix idea that every tool should do one thing and do it well is useful when it is applied to 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 broken into ten cooperating processes for the sake of purity.
Composability is good. Splitting a coherent tool into fragments because some philosophy says so is not. If the functionality belongs together, keep it together. The user should not have to wire three programs together to accomplish something that is obviously one task.
The instinct when something is not working is to add more. A flag, a fallback, a compatibility shim. Usually the right move is to understand why it is broken and fix the actual thing. More code on top of broken code is still broken code, just harder to fix now.
The same applies to features. A feature that does not get used is a feature that still has to be maintained, still has to be tested, and still has to be understood by anyone new to the project. Cut it.
When 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 sometimes. When they do, clarity wins over brevity, and correctness wins over both.
Names matter where they are visible. User-facing things, public functions, exported types, CLI flags, config keys, anything a caller or user touches should have names that say exactly what they are. A bad name on a public interface is a lie the code tells to everyone who uses it.
Internal code is different. A variable inside a 15-line function that nobody outside will ever see does not need a dissertation for a name. The developer working in that function has context. Use that context. What matters is that the code in front of you makes sense while you are in it, not that it reads well to someone skimming from the outside. That said, do not abuse this. If a better name costs nothing, use it.
State is where bugs live. Every piece of state is something that can be wrong, something that has to be kept in sync with something else, something that has to be reasoned about whenever you touch the code around it. Less state is almost always better.
Sometimes adding state makes things simpler. A flag that removes a whole branch of recalculation, a cache that flattens a loop, a counter that avoids a scan. That is fine, as long as you are honest about the tradeoff. You are trading correctness surface for performance or simplicity elsewhere. Know what you are doing and make it obvious in the code.
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 used by real people in real conditions.
That said, not every bug is a bug. Sometimes behavior that was unintended turns out to be useful, and trying to remove it breaks things people depend on. Use judgment. The question is whether the behavior makes sense in the context of what the software is supposed to do. If it does, it is a feature now. If it does not, fix it properly rather than patching around it.
A half-working feature is worse than no feature. If something cannot be implemented well enough to be 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.
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 documentation because the next person touching that code, possibly you in six months, will not have the context you have now.
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. Adding a comment that just restates what the code says is noise. It goes out of date, it gets contradicted by changes that nobody updated the comment 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.
Software does not exist to be admired. It exists to work. Elegant code that ships late and buggy is worse than ugly code that 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.
Everything else is negotiable.