You Might Not Need an Abstraction

Published 2026-02-05, About 14 minute read.

One of my all-time favorite Youtubers MPJ used to often repeat these words of wisdom:

The new engineer copies and pastes. The mid-level engineer makes abstractions. The experienced engineer gets s*** done.

- MPJ (paraphrased)

The thrust of his saying was that there is often a common path in the course of a software engineer's career. At first, they don't know what they are doing, so they emulate. Then they move on to using abstractions to dry up their code. Then they generally become pragmatic and don't worry about clever abstractions.

I'd say in the past two years I've been visited with the same thought over and over: You might not need an abstraction. More precisely, you may not need to make your own abstraction.

I've seen many engineers hampered in their efficacy because they become tied to "beautiful" and "clean" code. But I've also seen many wonderful examples of (in my estimation) great engineers and thinkers in our field that have rejected abstractions or avoided rushing to create them, often producing "wet" code. This "wet" code, though, is easier to work with and a pleasure to maintain.

Raise your hand if you ever tried to abstract fetch 📎

If you didn't raise your hand, I'll bet you don't remember the times you did, or you didn't realize at the time what you were doing! I've bumped into this knee-jerk reaction so many times in my coding career and seen so many devs do this that I would find it hard to believe it's not a rite of passage.

I actually bumped into this again in some recent work- it went something like this:

  1. "I know I'll be making these kinds of async requests often. Let me make this a primitive"

  2. "Okay, this is nice, but now I need to configure the fetch headers"

  3. "Cool. But I also want this thing to track its status with an enum."

  4. "How neat and clean and encapsulated! Now I want to be able to make callbacks for if the task fails, succeeds, or just resolves in general"

  5. "But wait! I need to cover cases where I change the base url as well!"

"How elegant!" I think and pat myself on the back. I do this for a while, only later realizing that I actually haven't made anything useful and created a lot of redundant code. Here's why:

I just re-implemented fetch. Compare using fetch v.s. my new "Task":

The fundamental value of abstractions 📎

Okay, I'm going to get mathy here (after all, at heart I'm a mathematician.) There is an information-theory-like way to describe the value of abstractions.

The Law of Abstractions!

For every piece of information or facet of an abstraction, there is a corresponding explanatory facet in either the scope, the arguments/inputs, or the context of the abstraction

It's kind of an law of entropy for abstractions. It might feel super obvious.

Here's a simple example:

The abstraction is a javascript function. It could be a class, a procedure, or any other thing. But let's just stick to a function. There is no input (maybe you could argue calling the function is a a facet), but there is a single point of output, so that must come from somewhere. The answer is that it's contained in the body of the abstraction. Here, it's just part of the function.

With this small change, the abstraction now can change who the hello is addressed to! There is basically two facets of the abstraction: the console log string but also the name. The console log is part of the abstraction body. The name is an explicit input to the abstraction. The "value" of this abstraction is to not have to re-write the console log in your logic, but also provide different names.

There are other things that can provide points of "input" that might seem like it's breaking the theorem.

The purpose of this example is to show how abstraction entropy is different from "functional" programming. I'm not claiming abstractions are functional here. This function would generate new results all the time and is clearly not functional. It does retain its entropy, though, in that the context provides the informational inputs for the Date (through the javascript Date API that the system uses- context) and the random number (through the pseudo-random algorithm the JS Math library provides- again, the context.) So, for the two console logs this function produces, we have clear inputs from context that map to them.

Going one layer deeper... what if we have a callback?

My argument is that the function provided is an abstraction itself. So, the inputs for the callback here are either abstraction body points of information or contextual points of information. Any outputs from the outer abstraction are informed by the inner callback which still follows the law. This means that the abstraction provided to the abstraction needs to be analyzed!

The corollary 📎

And, if you can accept that, here's my corollary:

Corollary:

Abstractions are "better" when facets are accounted for more in the context or body of the abstraction. In other words, abstractions do more for the consumer with less inputs.

The more a consumer of your abstraction has to do manually, the less valuable it is. The more a consumer needs to think about, the leakier your abstraction is.

What does this have to do with our task abstraction above? I'd argue that facets can be analyzed for the abstraction we made and compared to the facets of just using fetch

Let's create a table with the facets of the Task abstraction, and see where that information is accounted for in the abstraction body, arguments, or context. If the corrolary is true, the abstraction should be better than just using a fetch function if we find that the inputs are located in the context or abstraction more. If we can basically minimize our inputs, this is a good abstraction.

If we find we have the same inputs as fetch(), I would argue we've made no improvement

Facet Task Abstraction Fetch Abstraction
Domain to fetch from Consumer argument Consumer argument
Params Consumer argument Consumer argument
Fetch Configuration Consumer argument Consumer argument
Status Abstraction body Context of .then/.catch or branch of try/catch
Actions depending on status Consumer argument Consumer places these in `` .then/.catch or branch of try/catch
Task reuse Consumer calling a method again (start()) Consumer running async function again

The one thing you might argue is helpful is that you have runtime information about the task status in memory on the class instance. However, I'd argue this is not a distinct piece of information because you have the same information from just using promises (and, in fact, there are workarounds to get the status of a Promise, even though the status is not available programatically.) We've just moved the point of information from the context (Promises in Javascript) to the abstraction body (a property on the class.)

Similarly, the callbacks for certain actions on success, failure, and finally are just shifted to a different part of the abstraction.

At the end of the day, we aren't getting any leverage here. At best, I wrote an indirection with some mildly helpful information about its status. But really, I argue, It's just a bad abstraction.

Are you saying don't abstract? 📎

No.

There are a ton of very good abstractions and APIs that we use all the time. I'm not one to say we should all go back to assembly- that's foolishness.

I am saying that abstractions are hard to do well. When we think we should create a base class, or create a mixin, or a decorator, or if we think about writing a logging library... think about the value you are actually trying to encapsulate for consumers. If you've ever had to work with the wrong abstractions, you know it's a headache because it's actually making work harder.

When designing APIs you have to consider carefully what details you can encapsulate for the consumer. Those details can be "hidden" in the function, class, or whatever you're building. If the consumer gets an extra point of information out of it without having to include that point of information in it, you've gained some leverage

Abstractions and when they're worth it has been talked about ad nauseum in different forms. Dan Abramov has amazing examples in Goodbye, Clean Code and The WET Codebase. Joel Spolsky has written the seminal work on abstraction boundaries in The Law of Leaky Abstractions. There is also a well-known adage from Sandi Metz: "Duplication is better than the wrong abstraction." Doing a google search on youtube or google will give you more than enough videos and articles about this concept.

So what about fetch? 📎

The Fetch API in the browser (and recently node) is a good example of a good abstraction.

Take a gander at what a browser request would look like before and after fetch():

I would say that fetch() is better than an XHR request.

The one point of information that is assumed internally by fetch is the method "GET", which can be overridden as an input if need be.

The other piece of information that is assumed is that execution is implied by calling fetch. Fetch immediately returns a promise and executes the asynchronous process. The XHR request needs an explicit method call, which I would consider another facet that fetch provides.

Most importantly, fetch returns a Promise. The Promise is a contextual point of information, like Date, or the Math library. It also includes a lot of information on how to assemble the promise result, information about the xhr request, and methods to parse into JSON. To do the same with a Promise for an XHR request, you would need to implement a lot of abstraction body work yourself:

So fetch provides some common defaults and organizes the xhr response with niceties like status, ok, and json methods. The result is a promise, which provides a whole set of machinery to handle async requests. You can pass this promise around.

We get quite a bit out of this one function.

Summary 📎

At the end of the day, I think I'm suggesting that we avoid the tendency to over engineer. Look at the class, or the base class, or the decorator and ask, "What does this enable the consumer? What does this keep the consumer from having to do?"

I've spent a lot of time since I learned about the CAP Theorem trying to see if there's a corresponding theorem for abstractions and use by consumers. Admittedly, at times it feels a bit "hand wavey," but I think this is a lens to analyze abstractions and give more concrete explanations than just saying "this abstraction feels heavy" or "this abstraction is bad."

At any rate, I hope you enjoyed thinking about these things.

Find me on Bluesky or Mastodon. I also have an RSS feed here

⬅️ Previous Post
Back to top