[!NOTE] This is not an original post. It is a filtered and adapted take on the timeless wisdom found in Julio Biason’s article, “Things I Learnt The Hard Way (in 30 Years of Software Development)“
Wisdom doesn’t expire, but its application does. The original post is a masterpiece of cynical, hard-won experience. This is my version—the principles I’ve internalized, updated for the modern stack, and stripped of the baggage I believe no longer applies.
These are the unshakeable axioms. They are not subject to technological trends.
Clarity Before Code. If you can’t describe what you’re building in a few sentences of plain English, don’t open an editor. “Without requirements or design, programming is the art of adding bugs to an empty text file.” Spec first, then code. Always.
Code is Disposable. Your code is not an artifact. It is a temporary solution based on your current understanding of a problem. As that understanding improves, the code must be allowed to die. Attachment to sunk costs is the enemy of a clean system.
Solve the Problem You Have. Do not build for a future that doesn’t exist. Trying to solve speculative problems creates bloated, complex systems that are a liability. Solve the immediate problem, elegantly. Solve the next one. Patterns will emerge from necessity, not from prediction. This is the essence of YAGNI.
The Self-Collaborator. The most important person on your team is you, six months from now. That person has zero context, no memory of your brilliant insights, and no mercy. Documentation is not a chore; it is a love letter to your future self. Write it.
Atomic Commits. One commit addresses one logical change. Not three bug fixes and a feature. If you have to revert a change, it should be a clean, single-commit operation. git add -p is the tool for this. Learn it.
These are the actionable heuristics for building clean, maintainable systems at the micro level.
One Function, One Job. A function should do one thing. The easiest way to test this: if the description of your function requires the word “and,” it’s doing too much. Break it apart.
No Boolean Flags as Parameters. A function call like getUserMessages(userId, true) is meaningless without context. It creates cognitive overhead. Instead, create two explicit functions: getUserMessageSummaries(userId) and getFullUserMessages(userId). Clarity trumps brevity.
Types are a Form of Documentation. A type system isn’t just for the compiler; it’s for the human. It declares intent. [true, true] is a list of booleans. [1, 1] is a list of numbers. Don’t conflate them, even if the language lets you. Use the type system to make your data’s purpose explicit.
Comments as Scaffolding. If you’re stuck, write comments first. Describe the high-level steps in plain English. Then, fill in the code between the comments. Each comment becomes a block of logic, or better yet, a function call. This turns a complex problem into a simple to-do list.
The principles of good system design are timeless, but our tools for achieving them have radically improved.
Tests are an API Design Tool. The original post’s distinction between unit and integration tests is valid, but the implied friction is gone. For modern languages like Go, testing is a native part of the toolchain. Use tests not just for verification, but as the first user of your API. If your code is hard to test, its API is badly designed.
Code Style is a Solved Problem. The old wars over K&R vs. Google style are irrelevant noise. This debate is over. The rule is simple: use the standard, automated formatter for your language and move on. gofmt for Go, ruff format for Python, prettier for TypeScript. The tool makes the decision. No discussion. No ego. No review comments. We have automated this problem out of existence.
Organize by Feature, Not by Type. The old advice was to group all models, all controllers, all views. This is logical for libraries, but for services, a better pattern has emerged: organize by feature or domain. A user package contains the user model, handler, and storage logic. Co-location of related code is a higher form of clarity.
Understand Your Tools, Don’t Fear Them. The old advice was “Resist ‘easy’“. The modern principle is “Understand your leverage.” High-power editors like Zed and VSCode are not magic. They are built on open standards like the Language Server Protocol. Use their power to move faster, but retain the knowledge to perform any build, test, or deploy action from the raw command line.
Logging vs. Debugging. The original author’s cynicism about debuggers is an over-correction. They are different tools for different jobs. Logs are for production observability. They tell you what happened in a live system. Debuggers are for development insight. They let you interrogate a complex state at a specific moment in time. Use both.
Even as a solo developer, you are a human system. These rules govern its sustainability.
You Are The System. The code is a reflection of your mental state. Frustration, burnout, and ego lead to bugs and bad design. Self-awareness is the meta-skill. Recognize your own failure modes. If you’re getting frustrated, stop. Ask for help. Step away. Protecting your own clarity is the most effective way to protect the codebase.
Don’t Be a Hero. A “hero project”—an effort to prove a better solution—is a valuable learning tool. “Hero syndrome”—the belief that the system depends on you alone—is toxic. Build systems that don’t need heroes. Build them to be simple, observable, and resilient.
Recognize Sunk Costs. Know when to quit. This applies to a feature, a project, or a job. Continuing down a path because of the time already invested is a fallacy. Your most valuable resource is your future time and attention. Allocate it rationally.
Enjoyed the article? I write about 1-2 a month. Subscribe via email or RSS feed.