Sometimes reducing abstractions will make things cheaper.
Some regular traps that I see people run into with abstractions:
- building around scenarios that they can be pretty confident that they will never need to handle.
- building abstractions that are larger and more complicated than the thing they are abstracting (this tends to happen when people build abstractions of abstractions).
- building abstractions before they have a proper understanding of what they're abstracting and what they'll need to encapsulate.
In many of those cases, reducing abstractions (even just temporarily) reduces complexity.
When I'm building purely personal projects, I don't use abstractions to help me deal with directory structure on Windows, because I don't use Windows, and that would be additional complexity for no benefit -- so it's simpler for me to just work with the lower level OS paths.
When I start writing an abstraction I look at the amount of documentation I'm generating, and if I'm generating more documentation than it would take to explain the underlying system, I look to see if there are concepts that I can remove. I worked at a company where our build process became considerably simpler when we stopped using high-level build tools like Gulp/Grunt and switched to writing simple Node scripts, because it was easier to debug what those scripts were doing. We simplified that process even further by occasionally just dipping into Bash scripts.
Working with low-level concepts for a while often also gives you better understanding of what you need to abstract. I've worked with codebases where the abstractions all get built first, and it's not uncommon for those abstractions to be built around tasks that are pretty simple and easy to do with lower-level code, at the same time that the abstractions completely ignore the really difficult tasks that are very annoying to do. And once abstractions get baked in, it was time consuming and difficult and expensive to pull them out and rewrite them.
Going back to the scripts above, our build process got better because we made it simple to begin with -- a set of scripts, rather than a large established pipeline -- and then as we identified pain points, we started abstracting those pain points away. That allowed us to not waste time rewriting abstractions over and over and instead to have targeted small interfaces that helped us with the actual painful parts of building and deployment.
It is surprisingly common for software to be over-abstracted to the point where it is more complicated to deal with than it would be otherwise. I mean, heck, this comes up in web development all the time, it is one of the primary criticisms people have of the JS ecosystem -- that it overcomplicates development. In many cases those complications exist for reasons, they solve real problems that people have had. But also in many cases, someone's individual blog doesn't need any of that, and it would be cheaper and easier for them to build something smaller and simpler. If I can build a site that is one HTML file and one CSS file, and I know that's all I'm going to need, then it's overkill to try and set up a bunch of abstractions on top of that.