When adding new features to our applications, we are often faced with a choice: do we build it from scratch, or can we use one of the many solutions that are freely available in the form of libraries and plugins?
Depending on the ecosystem you are operating in, chances are that there is a vast collection of code that other developers and companies have made available either for free or for a price. Many problems have been “solved” by others, and we can use their solutions rather that having to come up with a solution of our own. If we do so, we don’t have to work on the same banal problems over and over again, and more complex problems are reduced to the usually much easier task of incorporating a turnkey solution into our projects.
While that sounds fantastic, assuming that every problem is made trivial by the abundance of libraries we can choose from is a dangerous path that can cost us much more time, effort, and therefore money in the long run than it saves us initially. I have even seen this approach wreck codebases so completely that the only viable way forward was to scrap large portions of them and start over, which is a costly undertaking in most cases.
Functionality that is made available to others is often extracted from either a single use case, or at most a very limited number of them. Ruby on Rails came out of 37signals building Basecamp and realizing that it would solve a large number of basic problems many developers face when building web-based applications. Material UI came out of Google defining a visual language for their Android operating system and realizing that the metaphors they identified could have a much broader application outside of their own identity. React came out of Facebook solving a particular problem they had in a small portion of their interface, and reducing it to a repeatable pattern that can be applied to many similar problems.
While these systems evolve to become more general over time, their origin is often of a much more limited scope. Unless ours overlaps with the archetypical use case exactly, we are likely to run into a scenario in which we need to tweak the functionality ever so slightly. Maybe our favorite user interface library does not cover all requirements of the interdependency between two fields in our signup flow. Maybe our existing database does not follow the naming conventions assumed by the object-relation mapping (ORM) we want to introduce, and we cannot adjust our database without undertaking an extensive refactoring across multiple applications that already talk to it.
Sooner or later, our use case and the one that led to the creation of the library in question will diverge in small but difficult to align ways. Which is perfectly fine: our challenges are as unique as the problems we are trying to solve in the first place. If they weren’t, there would be no need for the product we are building. We can judge the feasibility of an available solution by several factors:
Does it solve the problem well? If a library only solves our problem partially, we will have to invest additional effort to adjust it for our exact use case. Depending on how flexible or inflexible it is, that could result in more work than creating it ourselves would have caused. If it solves a much more complicated version of our problem, we will introduce complexity that adds to the cognitive load we expose ourselves to, making it difficult to retain a full understanding of all moving parts of our applications.
How frequently does it get updated? Unless the problem in question is really small, most libraries benefit from ongoing development that improves them over time. This development could introduce new functionality, increase the performance of existing features, or resolve bugs that were discovered after the release. If commits and releases are few and far between, it will most likely take a long time for outstanding and upcoming bugs to be fixed. We can get a glimpse of a project’s activity by checking the release history, the frequency of commits, and the time it takes for pull requests to get merged.
How many people work on it? We don’t always need a full team working on everything that we use. It is perfectly fine if something is not powered by Google or maintained by Facebook. At the same time, the greater the number of people working on a library, the more people will know its ins and outs and will be able to maintain it. If something is developed by a single person and that person decides to no longer support it, it will be dead in the water before long. GitHub makes this information very easy to identify, prominently displaying it in every repository.
How well is it documented and supported? If a library provides a large set of features, good documentation is absolutely essential to learn how to use it correctly and to refer back to when anything is unclear. If the documentation shows examples for all available functionality, outlines testing procedures, and is available in multiple languages, that again increases the familiarity we and other developers can have with it, increasing the pool of potential supporters. I also count the help provided in the project’s issues-section as well as questions on Stack Overflow as documentation. While they are spread out over the internet, frequent and helpful activity on questions surrounding a library are a strong indicator of a helpful and reliable community.
How much weight does it add to our project? While not relevant in all scenarios, the size of a library is particularly important in frontend-development, as any increase in size can negatively impact load times. An individual library might appear harmless, but a large number of seemingly insignificant increases in size can lead to “death by a thousand tiny libraries”. In this case, reducing the size increase in any way we can needs to be favored.
How much can we trust the code? When we add any dependency to our project, we add at least a few lines of foreign code that our customers get exposed to. We need to be able to trust that code to do what it promises to do and nothing more. By blindly relying on something, we sacrifice control and potentially the safety and security of our customers. We need to be able to understand the code and go through it in detail to confirm that it does not contain any suspicious functionality, which is straightforward in open source software and less so for closed source systems. An extensive test suite helps us understand what a piece of software does, while we still need to investigate it in its entirety to know that there are no hidden surprises.
How easily can we adjust it if we must? If we ever get to a point at which we need to tweak the functionality, being able to make those changes ourselves will be much easier than relying on a third party to do so for us. Being able to fork the library is helpful, but if it needs to be kept in sync with the original, that task will become our responsibility. We need to be prepared to invest the necessary effort to maintain those forks and apply all critical updates to them.
Is the code of a good quality? This intentionally comes last, because we can usually treat libraries as black boxes after we have added them. We do not necessarily need to know their internals to use them. In some cases, the quality of the code will impact both our and our customers’ experience with the product. Many WordPress-plugins add different and conflicting versions of jQuery to our pages, which can lead to bugs that are difficult to track down. We need to be particularly careful when a library makes changes to our own environment, such as when writing values to our database unchecked.
Despite some potential downfalls, it is always a case-by-case decision whether or not using something readily available works for us. There are some tremendous libraries and plugins out there for us to use, with many developers with much more expertise about a specific domain than us behind them.
None of the metrics outlined here are absolutely necessary, but they help to get a clear picture of what we are getting into. There are exceptions to these points, so we need to weigh them each individually each time we plan on using any library or plugin.
If something is managed by a single person but also publicly available using a permissive license, we can usually fork it and maintain an alternate version ourselves. We would lose the convenience of relying on others to evolve elements of our applications, but we will not be stuck using something that does not evolve with our application. Maintaining a fork also provides us with a baseline that we can build upon, rather than having to start from scratch.
If the problem a library is trying to solve is really small in size, it might be possible to consider it “solved”. Frequent updates might only be necessary to ensure ongoing interoperability with other libraries, which we would still need to confirm will be incorporated. If a solution is truly independent, not even these updates might be required.
If we are in a scenario in which we don’t care for the quality of a solution and prioritize speed of development over everything else, such as when prototyping a product that will be redone from scratch afterwards anyways, quality or robustness might be much less of an issue.
By writing our own implementation from scratch, we are not necessarily “reinventing the wheel”, but rather building the exact perfect sized wheel that we need. But if we’re not good at making wheels in the first place, it might be appropriate to rely on someone else who is much better at it. As long as we are aware of the implications and vet the options carefully, we can avoid a lot of hassle in the long run.