Why Good Software Thinking Starts by Distrusting Its Own Rules
Hatched by Jaeyeol Lee
May 20, 2026
10 min read
3 views
84%
The most dangerous sentence in software is not "I don't know." It's "This is the best practice."
Most people enter software expecting the hard part to be syntax, tooling, or algorithms. But the longer you work in it, the stranger the job becomes. The real difficulty is not learning a fixed body of knowledge. It is learning how to act inside a field where many of the loudest rules are partial truths, where the same advice can be wise in one context and harmful in another, and where the right move often depends on invisible forces outside the codebase itself.
That is why the smartest developers are rarely the ones who collect the most principles. They are the ones who learn to treat principles as hypotheses. They do not ask, "What is the rule?" They ask, "What problem did this rule once solve, and is that problem present here?"
This shift matters because software is not just a technical discipline. It is a living system made of people, incentives, legacy decisions, accidental constraints, and compromise. If you ignore that complexity, you may write elegant code that fails in practice. If you embrace it, you begin to see that good engineering is less like following a cookbook and more like navigation in fog.
Why simple advice breaks the moment real systems appear
New developers are often told to find a style, pick a framework, and follow best practices. This sounds reassuring, because it offers the illusion of clean answers. But software rarely rewards purity. A pattern that looks obviously correct in isolation can become clumsy when it collides with a production system, a product deadline, a support team, or an inherited architecture.
That is why so much software advice feels contradictory. One mentor says, "Abstract early." Another says, "Avoid premature abstraction." One post says, "Keep functions small." Another says, "Reduce indirection and stop over-modularizing." These are not signs that one side is ignorant. They are signs that software contains conflicting local truths.
A useful way to think about this is to imagine advice as medicines, not commandments. A medicine helps when the diagnosis matches, the dosage fits, and the side effects are acceptable. The same is true of engineering practices. Tests, encapsulation, microservices, event-driven design, type systems, code reviews, documentation, and automation are not universal virtues. They are interventions with tradeoffs.
The trap is turning a useful practice into an identity. Once someone becomes the person who believes in one right way, they stop asking the most important question: What situation am I actually in? At that point, the tool begins to use the engineer, not the other way around.
A best practice is often just the fossil of a past failure.
That is not a reason to dismiss it. It is a reason to understand it deeply.
The hidden story inside every rule
If you do not understand a practice, look for the horror story that created it.
Why do teams insist on code review? Because someone once shipped a disaster that was obvious to another pair of eyes. Why do engineers obsess over tests? Because a system changed in one place and broke in six others. Why do people warn against giant classes or brittle dependency chains? Because maintenance costs exploded when one assumption changed.
This changes the way you learn. Instead of memorizing a rule like "use dependency injection" or "avoid globals," you begin asking a better set of questions:
- What failure mode is this practice trying to prevent?
- What cost does it impose?
- What kind of system makes that cost worthwhile?
- What would be a lighter version of the same protection?
Once you start thinking this way, software stops feeling like a religion and starts feeling like risk management.
Consider unit testing. In a simple algorithmic module, unit tests may provide quick confidence with little overhead. In a tangled, rapidly changing UI flow, they may become fragile and expensive, while a smaller set of integration tests gives more value. The slogan "write tests" is too vague to be useful. The real question is, what uncertainty are you buying down?
The same applies to architecture. A microservices split may sound mature and scalable, but if the team is small and the product is still learning what it is, the real bottleneck may be communication, not service isolation. The cost of distributed systems then outweighs the benefit. Complexity is not a sign of sophistication. Very often, it is just deferred pain.
This is where many software debates go wrong. People argue at the level of doctrine when they should be arguing at the level of context.
Complexity is not the enemy. Unexamined complexity is.
There is a seductive myth in software that maturity means simplification. In reality, maturity often means something harder: the ability to distinguish necessary complexity from accidental complexity.
Necessary complexity comes from the problem itself. A payroll system must handle edge cases because real employees, tax laws, and time records are messy. A medical platform must be audited because consequences are serious. A large codebase must coordinate across teams because many people need to change it without stepping on each other.
Accidental complexity comes from our own decisions: poorly chosen abstractions, cargo cult architecture, duplicated workflows, unclear ownership, and interfaces built to satisfy a trend rather than a need. The challenge is that these two forms of complexity often look the same from a distance. Both feel hard. Both create friction. Both can make a project look "enterprise" or "serious." But only one is justified.
The real job, then, is not to eliminate complexity. That is impossible. The job is to spend complexity intentionally.
This is a powerful mental model. Think of complexity like money in a budget. You cannot avoid spending, but you can choose where the money goes. Every layer of abstraction, every framework, every rule, every process consumes cognitive budget. The question is not whether to spend, but whether the spend reduces risk, improves clarity, or increases the team's long term capacity.
A senior engineer is often someone who can look at a design and say, "This complexity has a purpose," or, "This complexity is pretending to be helpful." That judgment is not purely technical. It depends on product goals, team shape, organizational maturity, and even communication habits. A company with great support and domain expertise can often tolerate different design choices than a company where engineers must discover user pain through telemetry alone.
This is why talking to people outside engineering is so valuable. Support knows where users get stuck. Sales knows which promises are being made. Domain experts know the rules the code has to respect. The software may be written in one language, but the system it serves is multilingual. If you only listen to engineers, you only hear half the constraints.
The beginner's advantage is not innocence. It is flexibility.
New developers are often told to pick a stack, master it, and gain confidence. That advice is partly right. But there is another kind of growth that matters more: exposure to different kinds of programming.
Working in a few different environments earlier in your career teaches you something no single stack can. Backend systems teach you about reliability and data shape. Frontend work teaches you about user perception and the cost of change. Infrastructure work teaches you about failure, latency, and operational reality. Scripting or automation teaches you how much work disappears when you let the computer do the boring part.
These are not just technical experiences. They are different ways of seeing. Once you have moved across domains, you stop assuming that your favorite environment defines what software is supposed to be. You begin to notice that every specialty develops its own blind spots and its own myths.
That matters because the worst software mistakes are often not mistakes of skill. They are mistakes of perspective.
A developer who has only seen greenfield projects may underestimate legacy constraints. A developer who has only worked in large companies may overvalue process. A developer who has only worked on tiny teams may overestimate how far improvisation can scale. The ability to borrow perspective from elsewhere is one of the most underrated forms of technical competence.
The broader your exposure, the less likely you are to confuse your local environment with a universal law.
This is also why humility is not a soft skill add on. It is an epistemic requirement. If most software knowledge is contextual and much of the research is inconclusive, then certainty becomes a liability. The engineer who says, "This is the way," is often less trustworthy than the one who says, "Here is why this may work here, and where it might fail."
The best engineers do not worship uncertainty. They structure it.
If software is full of ambiguity, the answer is not paralysis. The answer is better judgment.
Judgment begins when you stop trying to eliminate uncertainty and start learning how to partition it. For example, if you are unsure whether a system needs a new abstraction, build the simplest version that can absorb change. If you are unsure whether a process is worth the overhead, try it on one team first. If you are unsure whether a performance bottleneck matters, measure it against actual user impact instead of theoretical fear.
This is what makes software craftsmanship different from rigid adherence. Craft is not the absence of rules. It is the ability to know when to bend them, when to preserve them, and when to replace them entirely.
A good mental model here is the difference between maps and territories. Best practices are maps. They help you navigate, but they are not the land itself. The moment you mistake the map for reality, you become vulnerable to every contour the map leaves out. Software rewards people who remember that the territory includes humans, deadlines, incentives, and history.
Another useful frame is failure ancestry. Before adopting any practice, trace its lineage backward. What breakdown produced it? Was it meant to prevent bugs, reduce coordination costs, improve readability, or save on-call fatigue? If you can name the ancestry, you can also see the boundary conditions. A practice designed to survive chaos may be overkill in a small repo. A practice designed for a team of two may collapse in a team of thirty.
This approach does not make you cynical. It makes you literate.
Key Takeaways
- Treat best practices as hypotheses, not commandments. Ask what problem they solve and whether that problem exists in your situation.
- Look for the horror story behind every rule. Understanding the original failure mode helps you use the rule intelligently instead of mechanically.
- Spend complexity intentionally. Complexity is inevitable, so direct it toward the highest value risks and constraints.
- Talk to people outside engineering. Support, sales, and domain experts reveal the real system your code must serve.
- Broaden your programming exposure early. Different kinds of work build different instincts, and those instincts make you less dogmatic.
The real goal is not to become right. It is to become adaptable.
The deepest mistake in software is not writing bad code. It is believing that there exists a stable, universal way to write good code, and that once you find it, the work is mostly over.
But software is alive in a very inconvenient sense. It changes as the product changes, as the team changes, as the users change, and as the company changes around it. In that environment, the best engineers are not those who defend a perfect doctrine. They are those who can keep learning without turning learning into self-image.
That is the paradox at the heart of the craft: you become more expert not by clinging more tightly to rules, but by understanding why the rules exist, where they fail, and how to adapt them without losing their purpose.
So the next time you hear a confident software maxim, resist the urge to ask whether it is true. Ask something harder and more useful: What kind of complexity is this trying to tame, and what would happen if I applied it here?
That question is where real engineering begins.
Sources
Hatch New Ideas with Glasp AI 🐣
Glasp AI allows you to hatch new ideas based on your curated content. Let's curate and create with Glasp AI :)
Start Hatching 🐣