We’ve been hard at work building out the software behind the 4Degrees platform for about 2.5 years now. It’s come a long way and we’ve received numerous compliments on the scope of what we’ve built in such a short time with a small team. To accomplish that, we’ve had to throw some of the conventional wisdom about software development out the window. You can see one example in a previous post on how “technical debt” isn’t real.
One of the peculiarities that we believe is critical to the fast pace of our development is an aversion to software dependencies. These most often take the form of frameworks or packages, like Django, pandas, Bootstrap, React, or Redux. They also include infrastructure dependencies, like a CDN, a managed database, or cloud-based machine learning.
Now, before you grab your pitchfork, we’ll be the first to admit that dependencies are absolutely necessary to modern software development — the end of this article will talk a bit about which ones we use and why. Our aversion isn’t a flat out rejection; it’s just that we place a high bar on our requirements for any dependencies and always make the choice to bring in a new one with a good deal of thought and skepticism.
There are many reasons that we’ve come to believe dependencies hinder effective development. They can be roughly summarized as:
- Dependencies are almost never as universally understood as their base technologies.
- The use of dependencies does not encourage competence among engineers.
- The proliferation of dependencies increases the chances of a critical failure of your infrastructure.
- By using a dependency, you are giving up part of your ability to improve your product.
Admittedly, these points are controversial. The prevailing wisdom in the industry today is “don’t reinvent the wheel”. The majority of the remainder of the article will explore these points in more detail, providing our perspective on why we think they make a compelling case for not using dependencies.
Lack of understanding
Almost by definition, almost all dependencies are less well understood than the core technologies they’re built on. Fewer people know pandas than know Python and the core list / dict data structures. Fewer people know Bootstrap than know HTML and CSS. Even where the dependency is very tightly linked with the core technology (like Rails and Ruby), there is almost certainly some population that knows the base technology and doesn’t know the dependency — whereas the opposite is generally not possible.
The problems of understanding compound as you add more dependencies to your system. In learning Bootstrap, a vanilla HTML/CSS/JS front-end engineer not only has to learn the HTML/CSS framework, they also have to come to terms with the jQuery baked into it. Engineers first exploring the Flask web framework we use at 4Degrees may be confused that they also have to learn the SQLAlchemy ORM and Jinja2 templating language as separate (but intertwined) dependencies.
This lack of understanding has real costs for technology companies implementing dependencies. In an industry where the average tenure in a role is less than three years, any additional requirements during the ramp-up period can substantially reduce someone’s contributions to the company substantially. Most hiring managers would agree that a new hire is not a net positive contributor to the team for the first three to six months. Do you really want to cut even further into an engineer’s 1.5–2.5 years of productive work on the team?
This ramp-up cost phenomenon is an even bigger issue at 4Degrees. We’re a young company growing rapidly, so the average tenure of our engineers is likely going to be lower than the industry’s average for quite some time. That means that the costs of the ramp-up period affect us even more deeply. We also have a strong culture of mentorship and contributing back to the community; we like to bring in junior interns and apprentices three to four times per year. If an apprentice is only with us for a few months for 20 hours each week, the last thing we want them spending their time doing is spinning their wheels in a framework they’re unfamiliar with.
Discourages competence
Related to the disparity in knowledge of frameworks and their base technologies, any time spent mastering a dependency may mean less time spent developing more fundamental skills or knowledge of the core technology.
Within 4Degrees, React Native has proven a perfect example of this pitfall. React Native is a framework for developing mobile apps that allows the engineer to program in a single language that is then deployable on both Android and iOS. The allure of React Native is obvious: you can write one code base where previously two would have been required. But React Native is distinctive among the many frameworks we’ve worked with — it has a massive pile of required knowledge that is distinct from its underlying technologies (in this case Javascript, Objective-C/Swift, and Java). In building out our React Native mobile app, we’ve had to spend a sizable amount of time learning the intricacies of React Native, as opposed to time that we feel could have been better spent learning the core tenants of mobile development on either iOS or Android.
While this point may be controversial, we would also say that many times it’s a bad thing that dependencies abstract away some tough problems. As an example, Bootstrap has a very convenient “grid” structure that makes it easy to horizontally distribute elements on the page. For experienced HTML/CSS developers, this ease feels like a dream in comparison to the pain of getting 3 variable width elements to look just right across various screen sizes in vanilla CSS. And that grid system is fantastic… as long as whatever you’re trying to distribute is cleanly divisible by 12 and you don’t need non-standard gutters or a more complex distribution with some fixed-width elements mixed with variable-width elements. In such cases (which are unavoidable in any medium sized web application), the front-end engineer that has come to depend on Bootstrap will find themselves foundering due to a lack of knowledge about the fundamentals of horizontal element distribution.
Increased chance of failure
With each added dependency in your project, the chances of (critical) failure increase. Say, for instance, that any given dependency has a .001% chance of being down at any given point (about 26 seconds per month). That’s not really so bad; many systems may have up to an hour or two of planned downtime a month. But your dependencies very rarely share downtime distributions, and pretty much no one has just one dependency. So if you have 10 dependencies, you may be looking at 4 minutes of average downtime per month.
Some experienced developers may laugh at the idea of having 10 dependencies. Any decently sized project probably has dozens. Of course, not all of those have any meaningful downtime averages — you can pretty much count on Python 3 running on Ubuntu 16.04 to not “go down”. Still, most projects have a host of dependencies with downtime probabilities that are modest on their own, but can add up when placed alongside dozens (sometimes hundreds) of similar dependencies.
It’s worth noting that this pitfall applies more to infrastructure dependencies than it does to software packages (though software packages have a similar problem when it comes to additive vulnerability). CDNs and servers fail more often than Bootstrap does. Any modern web application relies on the uptime of DNS servers, virtual machines, load balancers, and a variety of other nuts and bolts of the internet. And while all of these have solid uptime that you don’t much need to worry about out of the box, the concerns grow more and more pressing as you layer on complexity.
An example of some of the (regular) infrastructure failures we’ve had from dependencies at 4Degrees: Azure’s managed PostgreSQL service would go down for up to 15 minutes on average once a week, the CDN that hosts our third party datepicker goes down for up to 10 minutes about once a month, the CDNs that host Bootstrap and jQuery go down for very short amounts of time about once a quarter, our Azure CDN has had slow performance for up to 10 minutes about once a month. Heck, a huge chunk of Azure’s infrastructure even went down for about 10 minutes once in the middle of a customer demo.
Inability to improve
One of the biggest issues with bringing in dependencies to solve your problems is that you lose control over your ability to solve them. While whatever you’re bringing in may provide a solution to what you’re trying to do, you may learn over time that it’s not the right solution. When you’ve built out the solution for yourself, this isn’t a huge issue, just make the changes you need. But if you’ve brought in a dependency then you have to select from a few bad choices: strip out the dependency and start over, fork it if it’s an open source project (at the cost of potentially not being able to update it from the official repo going forward), or try to monkey patch it.
We’ve run into these challenges a few times at 4Degrees. Early on we were using a Python library for email address validation — we figured there were so many edge cases that we should rely on the expertise of someone who had spent longer on the problem. Over time we realized that the validater wasn’t catching all the cases we needed it to. After a fair amount of pain where the dependency wasn’t solving our needs, we stripped it out and started over from scratch.
We’ve had similar challenges with Bootstrap. The built in menus were fine, until they weren’t. The default modal is great, until we want to do something a little different. We’ve progressively replaced elements of Bootstrap’s framework. Although we’re still using it, we’re probably not too far from a future when we strip it out entirely.
So when are dependencies good?
You’ll notice that our skepticism of dependencies is fueled by our experience with technologies that we have used in the past — and will continue to use going forward. We have a bunch of dependencies at 4Degrees, so is it not hypocritical to say that they cause more harm than good?
The key to understand is that there is absolutely a time and a place for bringing in outside technologies; it’s just not nearly as often as we’ve seen done in the tech world at large. Let’s explore some of the factors that we believe lend themselves to “buy” instead of build.
Non-core expertise required
The most compelling reason to bring in an outside dependency is when building out the solution yourself requires a deep level of expertise that isn’t core to the success of your business. One classic example of this justification is encryption: building a good encryption algorithm takes years of study and often comes from seeking an advanced degree in the field. Trying to implement your own encryption without that expertise almost certainly means that you’re implementing insecure encryption. When combined with the fact that good encryption is generally a very easy dependency to install and use, it’s an easy call to bring in the dependency.
Substantial work savings
Work/time savings are the most-cited reason for teams bringing in dependencies. And rightfully so — no one’s trying to write their own operating system or program a proprietary database just to get their web app running. The systems we run on (Linux and PostgreSQL) are the results of decades of hard work by large teams of experts; the scale of the undertaking is too much to even consider taking on for a small team. The obvious systems to bring in under this rationale include the OS, the database, main programming languages, the web server, encryption, a web framework, a templating language, an ORM, and whatever other major systems are necessary to the functioning of your product. Beyond being way too much work to justify building them yourself, most of these dependencies require expertise that you shouldn’t be investing in (to the point above).
Beyond these core components, things can become fuzzy quickly. The past decade or so has seen a swift rise in the use of front-end frameworks like Angular and React, largely with the argument that their use saves a substantial amount of time over systems built from scratch with vanilla Javascript. At 4Degrees we don’t buy those arguments; 90%+ of our front-end dynamism has been written by hand.
The one major exception (or at least borderline decision) we’ve made in this domain is using React Native to build out our mobile apps. We spent a while on the decision and even after hundreds of hours spent with it, we’re still not sure it was the right one. The original rationale is that 1) no one on the team was skilled with Objective-C, Swift, or Java anyway and 2) the work savings of building a single repository instead of two were very attractive. In practice, we’ve found that the React Native framework has a lot of issues and not a lot of documentation, largely due to its relative newness and the original lack of intention on Facebook’s part to make it a new industry standard. We’ve probably still come out ahead on time invested compared to what we would have done with two separate code bases, but the math isn’t nearly as clear as it first seemed.
You’re going to have to redo it anyway
I mentioned before that one of the major downsides of dependency use is that it takes away your ability to easily improve the system. That pitfall is somewhat ameliorated in cases where you’re trying to get a quick solution out the door with the intention of replacing it in the medium term. Bootstrap is a great example of this at 4Degrees. We wanted our initial development focus to be on solving fundamental user problems, not making a site that looked pretty. We also recognized that when we brought in more design capacity that we’d wanted to create an aesthetic that was uniquely ours. Combining these two factors, it made sense to use Bootstrap to get a good visual system in place quickly with the intent of (gradually) replacing the components over time as we could bring quality design attention to bear on the problems.
In summary
Every engineering team has to figure out what systems and norms work best for them. Here at 4Degrees, we’ve figured out that we can get much more functionality pushed out the door if we use a high degree of skepticism in the introduction of new dependencies. We’ve also seen knock-on benefits in our own skills, ramp-up time of new team members, and the overall performance of our systems. That being said, we’re a small, unique group dealing with the specific challenges of an early-stage technology startup. Other teams may find more justification than we have for more prolific use of dependencies. Still, we feel that the industry as a whole has come to rely on dependencies a bit too much and would benefit from more thoughtfulness about the benefits and trade-offs of their usag