Stop Abstracting and Start Programming

How to avoid over-engineering and start loving the code
Thursday, September 11, 2025

How often do you find yourself writing code with a clear goal in mind when suddenly this annoying urge at the back of your mind tells you

Oh man this is so repetitive and single use, you really should abstract this, oh and this function up there man that function could be way more generic, oh you should totally use generics here, heck you know what make it all modular, do it now, abandon your task and do it now, you will thank me later.

That voice is an asshole, ignore it do not listen, it is trying to lead you down a path of pain and misery.

Why Over-Abstraction Hurts ?

As programmers we have this idea in our mind that eventually this code you are writing right now will be reused by yourself or someone else in the future and obviously you’ll want to make it as reusable and as generic as possible. So that when the time comes you can just plug and play it into something else with minimal effort and complete whatever task you have at hand with relative ease.

The problem with this mindset is that it’s mostly true but can easily be overdone to annoying extremes. You can easily end up in a situation where you have a codebase that is so abstracted and generic that it becomes impossible to understand what the code is actually doing. You end up with layers upon layers of indirection, making it hard to trace the flow of data and logic. This can lead to increased complexity, making it difficult for new developers (or even yourself) to understand and maintain the code.

I’ve worked on countless codebases in the past where just changing something as simple as a label required me to dig through 5-7 levels of files. This is not ideal and it’s really hard to build a mental model of the codebase when you have to jump through so many hoops just to understand what a piece of code is doing.

In fact…

Anecdote

I once worked on a project where I had to change the text on a single button. Should’ve been a two-minute job. Instead, it turned into a nightmare. The label wasn’t in the component. It wasn’t in the props. It wasn’t even in the constants file. Nope. It was hidden behind a “UIContextProvider” that wrapped another “GenericLabelRenderer” that passed down to a “LocalizedStringFactory.” By the time I finally found the damn string, I had clicked through six different files and completely lost track of what I was even trying to do.

That’s the cost of over-abstraction. Something dead simple became a multi-hour scavenger hunt because someone thought they were being clever by making the text system “flexible” and “reusable.” Spoiler: nobody ever reused it.

When to Abstract your Code ?

This is a tricky question and the answer as always is well it depends. But there are some general rules and guidelines that personally help me be productive and avoid over-abstraction.

Write it First, Abstract Later:

Start by writing the code in a straightforward manner. Focus on getting the functionality working first. Once you have a working solution, you can then look for opportunities to abstract and refactor.

YAGNI You Ain’t Gonna Need It:

Avoid adding abstractions for features or use cases that you don’t currently need. It’s easy to fall into the trap of over-engineering for hypothetical future scenarios. Only abstract when you have a clear and present need for it.

Keep it Simple:

Strive for simplicity in your code. If an abstraction adds unnecessary complexity without a clear benefit, it’s probably not worth it. Simple code is often easier to understand and maintain.

Limit the Levels of Indirection:

Try to keep the number of layers of abstraction to a minimum. If you find yourself needing to jump through multiple files or layers to understand a piece of code, it might be a sign that the abstraction is too deep.

Use Descriptive Names:

When you do create abstractions, use clear and descriptive names. This can help make the purpose of the abstraction more apparent and reduce the cognitive load when reading the code.

I especially find the most value in the first point, usually if you just make it work first even if it’s a quick and dirty implementation that just proves it works. You can really easily find which parts require refactoring and abstraction once you are done with the initial implementation, and from what little experimentation I’ve done with this technique I found that I end up finishing my work a lot faster than if I had tried to abstract everything from the get go. Mostly because this way I avoid the dreaded analysis paralysis where you just keep thinking about how to make something generic and reusable instead of just getting the job done.

And that last part is really crucial I know I am repeating myself over here but it’s really easy to find yourself spiraling into a rabbit hole of generic functions and complex what if scenarios that drive your architecture into a direction that is not really useful for the task at hand.

Conclusion

Abstraction is a powerful tool in software development, but like any tool, it can be misused. By focusing on simplicity, writing code that works first, and being mindful of when and how to abstract, you can avoid the pitfalls of over-abstraction and create code that is both maintainable and understandable. Remember, the goal is to write code that solves problems effectively, not to create the most generic and reusable code possible. So next time you feel that urge to abstract everything away, take a step back, breathe, and ask yourself if it’s really necessary for the task at hand.

Wanna show support?

If you find my sporadic thoughts and ramblings helpful.
You can buy me a coffee if you feel like it.
It's not necessary but it's always appreciated. The content will always stay free regardless.