Stephan Schmidt - September 6, 2025
Abstraction as a Developer Footgun
Why premature abstraction creates tight coupling and technical debt that slows down development
Abstraction is one of the most favorite developer footguns. Developers shoot themselves in the foot left and right with abstraction.
Whenever a developer sees something twice, they smell an abstraction. Even worse, developers often start with an abstraction right away,because they ASSUME potential duplication in the future.
But even from seeing something two times, you don’t know enough about the thing to abstract it away. While they think of all the benefits of abstractions to readability, changeability and pure beauty, abstractions are creating tight dependencies that make all progress more difficult, if it’s not in lockstep with reality.
The way developers abstract is based on structure, which means code lines or method signatures. In reality, two things with the same structure - AT A RANDOM POINT IN TIME - might be totally different things. And after some short closeness, might develop into very different directions. Or on different time lines. These two things might have very different variability - one doesn’t change while the other changes often. DRY is often the wrong advice and repeating yourself, repeat the structure because it has different tangents, is a good thing.
As a guideline: When abstracting, abstract the things that have the same variability, and the same future tangent, not the same structure. Seeing something twice is too early for any abstraction, because you haven’t seen enough to predict the future. I’m not claiming I’m right, it’s just my opinion, but I have been writing code for 45 years now, and this is my current understanding on this issue.
They are also leaky.
A story of an abstraction
A developer sees two instances of the same code
fn f() {
s := a + b
}
fn g() {
s : = a + b
}
and thinks, “I can abstract this away”. So they create an abstraction for adding two numbers.
fun add(a int, b int) int = a+b
fun f(add fun) {
s := add(a,b)
}
fun g(add fun) {
s := add(a,b)
}
to add two numbers and make the functions f
and g
use the adding implementation to add two numbers. They have created a dependency that binds them from now on. Two functions depend on a third one wheres in the beginning there was no dependency.
The developer might think that the signature of fun add
is too restrictive, too concrete, not abstract enough, limiting. They add
type adder(a int, b int) fun
to use different adders for different use cases. A fast adder, a precise adder, and more. Abstracting the method signature to
fun f(adder fun)
This creates a second dependency that isn’t necessary, but driven by the developer who thinks they can predict the future and there “surely!” will be a need for different adder functions. We no longer can change the add
functions signature without changing the adder
type.
Later on we get another function
fun twoadd(a,b) = 2*a + 2*b
and splendid! Our abstraction works, as predicted! We can reuse f
and use f(twoadd)
instead of writing a new twof(...)
.
Disaster strikes when twoadd
becomes threeadd(a,b,c)
, threeadd
no longer fits the adder
type, now what? We need to add another abstraction on top to again keep the two cases abstracted, e.g.
type adder(summands []int) fun
And this goes on and on. To keep diverging code tied into one abstraction, where parts move at different speeds or into different directions,we need to pile more and more abstractions on top of each other to keep it working.
Another example for wrongly predicting the future
A product manager (PM) wants the developer to draw a triangle in the app. And the developer draws a triangle with fun triangle()
. After some time, the PM says “It now needs to be a square”. And the developer changes the code to fun square()
to make the app draw a square. The PM comes a third time and says “It now needs to be a pentagon”. And the developer changes the code to fun pentagon()
to make the app draw a pentagon.
Now the developer thinks, “Wait a minute! I see where this is going. Why always rewrite the code (and all that mathematics). Lets’ abstract what the PM wants, what I will need is a function
fun ngon(n int)
and then I can do whatever the PM wants.” So they abstract away and write the ngon
code during a sprint. And they are living happily ever after.
Sorry, no. The PM comes again to the developer and says “The market has changed, now we need a rectangle”.
And the developers code can’t do that. And they throw away the ngon
code and write a new rectangle
code. Or worse, they wrangle the ngon
code into a rectangle code with some IFs
or a ngon(n sides, side lengths[])
. The developer chose the wrong abstraction. The developer thought they new where things are going, but they didn’t know that the market would move to rectangles.
This is simple to see with rectangles and add
functions. In reality I’ve seen this with huge modules, complicated interfaces and type hierarchies that bind all code together and make changes more difficult, instead of being abstractions that make changes easier. And at one point a CTO needs to tell the CEO that the feature that should take a day will take three weeks, because of an entanglement of abstractions that can’t be changed without rewriting huge parts of the code.
- no ads, no sponsorships, free.