Photo by Asa Rodger

A mistake more expensive than ‘null’

Jarrod Overson
6 min readApr 17, 2021

Every day we use a programming pattern that makes software needlessly expensive to build and maintain. It causes countless bugs and security vulnerabilities. It needs constant refactoring. It’s difficult to test, it’s tedious to document, and it’s flexibility makes every implementation a unique snowflake that leads to unending code duplication.

It’s the function.

More specifically, it’s the interface we expose which is commonly a collection of functions.

Which languages do you recognize?

One of the first things we learn when programming is how to reuse logic. This invariably leads to the function, the building block of all software. Functions aren’t bad all on their own, but relying on them as the primary reusable component is the reason why software is so expensive to write, maintain, and scale.

Why?

It is difficult to write reusable code.

No, that’s not right. That doesn’t sit well.

Any developer can write reusable code regardless of experience. Languages like JavaScript, Python, Ruby, and Go are built on millions of small, shared modules that show how easy it is to write reusable source code. Writing reusable code is easy.

Let’s refactor.

I̶t̶ ̶i̶s̶ ̶d̶i̶f̶f̶i̶c̶u̶l̶t̶ ̶t̶o̶ ̶w̶r̶i̶t̶e̶ ̶r̶e̶u̶s̶a̶b̶l̶e̶ ̶c̶o̶d̶e̶.̶
It is difficult to reuse code.

Well that’s not it either. Take a look at the node.js library repeat-string on npm. It does nothing more than repeat a string and developers download it over seventeen million times every week.

repeat-string download count on npm

Seventeen million is only the download count. It doesn’t even begin to illustrate the scope of how many times that function is actually reused in source code. It must be astronomical!

So what am I getting at?

How would you find a module like repeat-string for your node.js project? You’d search for “repeat string” on npm, right? Maybe you’d search for “string repeat” but the results would be similar. You can see the problem I described above in the second search result. And the fourth. And the ninth. And tenth. And eleventh.

Take a look at these examples. Each library provides the same exact behavior.

Examples for string repeat libraries on npm

Do you see the problem?

No, it’s not that one of them is asynchronous for some some strange reason. Nor am I talking about how string repetition has been part of the JavaScript language for 6+ years ("A".repeat(5)). It's that each library has differences across:

  1. Their input signature. Some take (string, int), others take (int, string). One actually accepts floating point numbers and another needs a callback.
  2. Their output signature. Each output a string except one which returns nothing and delivers its result in a callback. Don’t even get me started on their errors.
  3. Their execution behavior. One is async others are synchronous.
  4. How they are exposed. Some expose a single exported function, others expose it as a method on an object.

It’s easy to pick on JavaScript for many reasons but this isn’t a language problem. JavaScript’s popularity and meager standard library makes this issue so prevalent I can demonstrate it with something like string repetition. Nothing in other languages prevents me from replicating these differences. This is a very simple example, but such differences only become more pronounced with more complex functionality.

What’s the big deal?

The problem is these choices add zero value, only friction. They have no impact on “business logic” (a.k.a. the stuff that matters). They are implementation details. Sometimes these choices make up for deficiencies in a language or framework and sometimes they’re just the result of a mental flip-of-a-coin. We try to choose the path that improves the developer experience now or in the future, but predicting the future is impossible. These decisions are diabolical in the way that every choice feels good at first, while everything works. We get a dopamine hit and feel like kings and queens. Masters of all things computery! When we need to add, replace, or change something though, we realize we weren’t quite the geniuses we thought.

How often do we need to change something? Every day. That’s all we do. How often does software break because of it? Literally all the time. Sometimes the break is minor and we fix it without thinking. We accept that broken software is normal. We even write automated tests that do nothing except point out something broke.

The interface friction problem stems from the countless junctures where a programmer can make a decision during development. Decisions like positional arguments vs configuration vs builders, async vs sync, global vs local, stateful vs stateless, static vs instance, constructors vs factories, callbacks vs futures, functional vs object oriented, events vs anything and infinitely many more. Each choice also has a unique flavor for that day’s snapshot of best practices and each is a grain of sand in the machinery.

This frustration isn’t novel, yet we tolerate it and teach it to each generation. Why? It’s not that we make bad decisions, it’s that we have bad choices. Every option is a kaleidoscope of tradeoffs and the right answer looks different when viewed from any other angle. When everything is a compromise, there is always a better choice. There is always a better way to rewrite code.

Let’s refactor the problem statement a third time.

I̵t̵ ̵i̵s̵ ̵d̵i̵f̵f̵i̵c̵u̵l̵t̵ ̵t̵o̵ ̵w̵r̵i̵t̵e̵ ̵r̵e̵u̵s̵a̵b̵l̵e̵ ̵c̵o̵d̵e̵.̵
̵I̵t̵ ̵i̵s̵ ̵d̵i̵f̵f̵i̵c̵u̵l̵t̵ ̵t̵o̵ ̵r̵e̵u̵s̵e̵ ̵c̵o̵d̵e̵.̵
It’s difficult to write interchangeable code.

It’s not catchy, but it’s getting closer.

Without plug-and-play code, we need to iron out every wrinkle in an interface before we can use it. We have to customize every bit of code that goes into an application. Every input. Every output. Every API. We put builders on adapters and wrap them in factories behind services. No amount of lipstick can make your design pattern beautiful.

It is like we’re building software in the 18th century. We cut trees by hand to boards with arbitrary dimensions. We craft hammers from scratch and press iron into nails just to build a house identical to the one next door. It costs too much and takes too long. Even once finished, we’re still burdened with bespoke maintenance. Our dimensions are non-standard, the wiring keeps sending electricians to the hospital, and the builders fresh out of trade school don’t like the way we made nails. We’re overdue for a software Home Depot and digital 2x4s.

Which API do you need to look up over and over and over again?

It’s not far-fetched to think we could have standards that work well enough for everyone, where everything just fits. Microsoft introduced COM in the 90s to share logic across applications written in any language. The JVM is almost as old and has shown how multiple languages can target the same bytecode and interoperate with each other. The world has been discovering and rediscovering Flow and Linda for decades as a way to connect distributed black boxes, and Docker has revisited what a modern black box can be.

There’s a lot of opportunity in this problem and many are trying to solve it. Dapr and WasmCloud are promising next iterations. They’re both solving part of the problem in different ways. As sad as the state of software is right now, I’m more hopeful now than I have been for the past 10 years. JavaScript provided a brief period of exhilarating promise — where a universal platform could allow amorphous applications to spread across the globe— before collapsing into a heap of Electron, React, and spent RAM. Web Assembly is my source of new optimism. It’s far from perfect but that’s what’s exciting. Perfect never wins.

--

--

Jarrod Overson

I write about JavaScript, Rust, WebAssembly, Security. Also a speaker, O'Reilly Author, creator of Plato, CTO @Candle