I’ve been revisiting some of my older codebases lately, and something keeps hitting me: most of the problems I solved didn’t need to be solved at all. Looking back at thousands of lines I once thought were genius architecture, I now see complexity that actively hurt the systems I was building.
The Engineering Ego Trap
Early in my career, I measured my worth as a developer by how much code I could write. Complex abstractions, elaborate patterns, layers upon layers of indirection. If my code was hard to understand, I figured it must be sophisticated. Professional. Smart.
Here’s the uncomfortable truth I had to learn: the best code I ever wrote was the code I deleted. Not the clever algorithms. Not the flexible factories. Just… less.
What Deletion Actually Gives You
When I started actively deleting code instead of adding to it, something magical happened:
- Fewer bugs. Every line is a potential bug. Delete it, eliminate the possibility.
- Easier onboarding. New team members don’t need to understand your clever abstractions to make simple changes.
- Faster iteration. Less code means less to reason about when making changes.
- Better performance. Usually, simpler paths through the codebase are also faster.
When Deletion Gets Hard
The hardest deletions aren’t the obvious cruft. They’re the things that felt necessary at the time:
The “generic” solution that handles 3 use cases but you only use 1
The abstraction layer added “for flexibility” that’s never been swapped
The configuration option that was supposed to be used but never was
The helper function that’s called exactly once
The Abstraction That Never Was
I remember building a “pluggable notification system” for a project. It had email, SMS, push notifications, webhooks, and a message queue. All nicely abstracted behind interfaces. The implementation took weeks.
You know what we actually used? Email. Just email. The other 80% of the code was dead weight that made every change take longer because we had to update interfaces, implementations, and the wiring between them.
That abstraction wasn’t foresight. It was over-engineering.
A Practical Heuristic
Here’s a question I now ask before adding any code: “What would I have to delete to make this unnecessary?”
Often, the answer is: quite a lot. And that’s usually a sign that maybe the problem being solved doesn’t actually exist yet, or exists in a simpler form than we’re preparing for.
I’ve started applying what I call the Wait-And-See Tax: instead of building for future requirements, I accept that I’ll pay a small cost later to refactor when (if) those requirements arrive. That tax is almost always cheaper than the cost of maintaining unnecessary complexity upfront.
The Deletion Practice
Every month, I spend a few hours looking for code to delete:
- Features that aren’t used (check your analytics/tracking)
- Dead code paths (conditional branches that never execute)
- Unused configuration options
- Over-abstractions with single implementations
- Comments that explain what the code does (if the code needs explaining, fix the code)
It’s oddly satisfying. And each deletion makes the codebase a little more maintainable.
Final Thought
Code is a liability, not an asset. The only code that truly adds value is the code that does something necessary, and does it clearly.
Everything else? It’s debt wearing the mask of functionality.
Next time you’re tempted to add another layer, another abstraction, another configuration option — pause. Ask yourself: what if I just… didn’t?
