Less Dependencies
As I grow and learn and practice software engineering, I have come to appreciate the practice of avoiding dependencies whenever possible. There are smart people out there who aim as low as zero dependencies. Honestly, I tend to agree with that philosophy and goal. Anytime I can remove a dependency from my project, I’ve probably saved my future self a headache.
What is a dependency? For this discussion, a dependency would be any package, library, or framework that my software project is using. Some dependencies are going to be mostly unavoidable, like a compiler, and some dependencies can and have saved me time and effort. Even still, I always use caution when thinking about adding a dependency.
Why avoid dependencies? I’m not sure I can say it better than the links above but here are a few things I tend to think about:
- How fast does my dependency deal with CVEs? What about the things my dependency depends on?
- What is the footprint (install size and runtime cost) of the dependency? Do I actually need all that functionality or just some of it?
- Will updates to one dependency conflict with others? For example, if I have a React project that is using several libraries written for React 16.0 then how much of my site will break when I update to React 19.0?
- How long has the dependency been around and how likely will it continue to be supported? Just thinking about this upfront helps me recognize the cost or risk of using that dependency.
Litmus Test
My personal litmus test for dependencies is: could I implement this myself? I tend to choose a dependency that I could implement because I can understand it. I’m weary of using things I don’t fully understand. That question also helps me think about how hard it would be to replace that dependency, if needed, and also to make sure the dependency or it’s API does not provide a whole bunch of extra things I don’t need.
Anyway, that question may sound like a funny test of a dependency so let me explain a little more with a story.
About 8 years ago I was working on an internationalization project for a React-Native application. My team was building a tool to help other teams localize their content in our app. I don’t remember all the options we looked at but I do remember two specific options: messageformat-js and i18next. On the surface, i18next looked very complete and polished while messageformat did not. The company I was working for had lots of specialized requirements so I wrote some code to try them out. I had a hard time customizing i18next to meet our needs because the API was designed to fit a certain use case and it was hard to adapt to ours. On the other hand, since messageformat was at that time, not much more than an ICU message format parser, it was much easier to work with. In fact, building our customizations was pretty straightforward because the library itself was straightforward. We ended up using messageformat and it served well for the few years I followed the project. Years later, I would expect someone would have replaced that library but I would bet the upgrade was straightforward because the library was too.
Okay, the above story may not be a good illustration but that question (could I implement this library?) was part of my thought process. As I looked at the API and functionality that i18next offered, it became hard for me to imagine how I could build that myself. I prefer dependencies I understand because if I don’t understand it, I may have a hard time replacing it. On the other hand, I quickly understood the messageformat-js API and how I could build it which meant that, if I needed to replace it, I could.
Learning from Dependencies
One reason I like to use a dependency is to learn from it. In Tim Bray’s post, he talks about not writing your own crypto library. Very true, by using a standard dependency, you are benefiting from the expertise and experience of many smart people and benefit from years of performance and security fixes. I could see a math library (like Matrix or Vector math) being much more performant than implementing your own. Using these dependencies will teach you about API design and how the underlying technology works. A dependency lets you focus on your specific problem rather than having to understand a whole other complex topic (like cryptography). These types of dependencies fail my litmus test but that is okay. That test is just one way to think about it.
Another reason to use a dependency is to accelerate your project short term. With Dunshire DOOM, I took a dependency on kaitai which is a library for reading binary files. I wasn’t interested in reading binary data in JS and they already had a DOOM WAD file example on their site. Using katai let me focus on more interesting things like rendering the maps or writing monster AI. As I continued to work on the project though, and started reading more exotic community maps, I started writing JS code to read the binary files and found it pretty straightforward. I realized it wouldn’t be hard to remove the katai dependency so I did. Removing katai reduced the JS bundle from 1.6MB to 1.3MB and removed 800 lines of code. That’s right, removing the dependency resulted in shorter code. This is because a library has to handle many use cases where a bespoke implementation can be more focused. If I hadn’t started with katai (or something else), I may have been bogged down in the WAD file format and lost interest in the project.
Final Word
Like most things in life, there are very few rules that are right all the time. Generally, you will have some wiggle room. Not all dependencies are bad all the time. There is always the “choose the right tool for the job” argument. The “engineering” part of software engineering comes down to building and delivering software within a set of constraints. Sometimes you are constrained by time, sometimes cost, or sometimes by the deployment runtime - are you targeting a device with limited RAM or bandwidth? Each of these constraints should impact what dependencies you choose and, in fact, whether you choose to use those dependencies at all.
In general, avoid dependencies. On the other hand, sometimes they are helpful to get you where you need to go but be mindful of the risks and plan to maintain or remove them.