Join 5000 CTOs and engineering managers for opinionated insights Subscribe

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.

Regular triangle, square, and pentagonThree regular polygons with equal side length of 100 units: an equilateral triangle, a square, and a regular 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.

My Book for CTOs Amazing CTO Book
Join 5000 #CTOs and engineering managers for weekly insights
- no ads, no sponsorships, free.