Simplicity in software is not a destination — it is a constant negotiation. Every abstraction you add buys you something and costs you something else. The cost is usually paid later, by a different person, in a different context, when the original tradeoff has long been forgotten.
The pull toward complexity is real and mostly well-intentioned. Engineers reach for abstractions because they have seen the pain of repetition. They add configuration layers because they have been burned by hard-coded assumptions. They build frameworks because raw code felt fragile. The problem is not the instinct — it is the timing. Most abstractions are introduced before the pattern they are meant to capture is actually understood.
A better heuristic: write the thing three times before you name it. The first time you are discovering the problem. The second time you are solving it. The third time you understand it well enough that an abstraction would be honest rather than speculative. Premature abstraction is just technical debt wearing a clean API.
Simplicity also does not mean small. A well-structured 2,000-line module can be simpler than a 400-line tangle of micro-utilities that all import each other. The measure is cognitive load — how much you have to hold in your head to reason about a change. Simple systems let you make a change in one place and trust that the rest of the system is unaffected. Complex systems make you afraid to touch anything because the blast radius is unknown.
The hardest part of simplicity is that it requires you to say no. No to the feature that sounds easy but bends the data model. No to the abstraction that feels clever but only saves three lines. No to the configuration option that covers a case that has not happened yet. Simplicity is an act of editorial judgment, and editorial judgment requires the confidence to cut things that have not yet caused pain.
Ship the simple thing. If it breaks, you will know exactly where to look.