Lessons Learned From Writing an In-House Library


This article was written by Erich Jagomägis, the Tech Lead at Bitweb, a partner of Digit 2024

Nowadays, any codebase relies on libraries. Libraries are an essential toolkit programmers possess to deliver complex features for a fraction of the price. For a library to be adopted by as many teams as possible, it must be helpful and convenient. Many properties describe a well-written library. In some cases, it is not feasible to write a library that would be acceptable to the general public, or decisions made during the development of the library are heavily opinionated and sometimes even controversial.

A library is a set of functionality and data structures written by someone. It provides value to the developer and, by extension, to the product owner by freeing up development time that would have been used to write said functionality. A well-thought-through implementation that has passed the trial by fire is more reliable and cheaper than an implementation written as a means to an end to satisfy a functional requirement.

In-House Libraries: Tailored Solutions

Some of the functionality we are looking for is unavailable in third-party libraries, and developing a third-party library with best practices in mind might not fit into the budget provided for the project’s development. Moreover, said functionality might be heavily opinionated and tailored to the specifications that best suit the project or company’s needs. We call such libraries in-house, as they are aimed to accommodate the company’s needs.

It must be clarified that we are not talking about project libraries. Project libraries may contain business rule routines shared across multiple code bases within a single project. In-house libraries are business/field agnostic in that regard.

Adoption and Best Practices

Having worked with many teams from different companies, we have learned that writing an in-house library is not a widespread practice. There can be several reasons, but the most prominent is that developers are reluctant to take the initiative, and project managers need to be better versed in the technicalities to see the benefits of doing so. That becomes painfully obvious while developers copy code from one application’s codebase to the next. It might seem harmless with a single snippet, but less so when the same component is copied across a dozen applications. Sometimes, that component might get a bug fix in one codebase, but a copy is still selected from an unfixed codebase.

Writing features in an in-house library should follow the same principles and good practices as writing for the public—anything less would be disrespectful to our colleagues. Developers’ loyalty lies in the scope of responsibility. If libraries cannot fit into that paradigm, they will be left behind in favour of other solutions.

It is interesting to observe how developers use the features you wrote. The best feeling is to see your feature being used as intended. However, sometimes developers will not do so. There are various reasons for that to occur. Sometimes, the provided functionality is close to what is needed, but something is missing.

Other times, the API could be better designed, making adoption frustrating. Vague/incomplete documentation cannot be beneficial either. Different principles could be followed to help design features that are convenient to adopt. Moreover, certain features should be documented well to avoid developers reimplementing something already supported or misusing the provided functionality.

Managing Features and Dependencies

Analyzing how features are misused has given us insight into how we should design them. A set of principles can be followed to allow adopters to make the most of the provided features and build onto existing features to accommodate all their requirements.

How can we achieve that and ensure that future changes to the codebase don’t break everything for the dependents? These goals are achievable by following clean code principles and imagining what could be done with the provided features.

Auto-Configuration and Flexibility

Some libraries behave like plugins to the underlying framework onto which the application is built. Such libraries use auto-configuration to set themselves up frictionlessly, which can be very convenient for the developer. However, having many different features in a library can have adverse effects if all these features attempt to auto-configure automatically. This can negatively impact the adoption process and the final shipped product. There are different approaches to how such challenges should be tackled.

Configuration is an important mechanism that allows developers to tailor a feature to the requirements of a specific use case. Therefore, configuration design shouldn’t be an afterthought; it should lay out the blueprint for how the components making up the feature should be designed.

Configuration should provide flexibility, but it should also protect the developer from misconfiguration or caution against shortcuts that might affect the quality of the product.

Dependency Conflicts and Resolutions

Libraries depend on libraries to provide new features. Dependencies between libraries and our codebase make up something that is called a dependency tree - an acyclic graph representing relationships between components. Moreover, the dependency is not only on the library/component but also its version.

Dependency trees can directly impact both the development process and the final product. Having a dependency that transitively loads too many libraries into the application’s context or having dependency resolution conflicts are just some of the issues we must consider when building an in-house library. Transitive dependencies can cause harm when the developer uses components from them in the codebase.

Such issues can negatively affect the library adoption process. To solve these issues, we have chosen a rather unconventional solution.

Ensuring Efficiency and Quality

With a new in-house library, developers can become eager to add anything that can be reused more than once from the project’s codebase to the in-house library. Even though the intent is good-hearted, this can bloat the library to include many features that a marginal number of dependents will adopt. Having a bulky library is hard to work with, and we pay a lot of attention to which features make it to which libraries.

Lastly, we thoroughly test our features with automated tests. Using library features, developers must write fewer tests as they don’t have to write tests for the code they don’t own. This is a nice side effect of having a smaller code base, providing faster business value delivery to our partners.

It is hard to estimate how many bugs we mitigated or how many work hours were saved by writing and propagating an in-house library across tens of projects, each with a dozen code bases. Still, I can say that being able to say, “Hey, we have exactly what you need. Simply add that dependency and add one row into the config file, and you are set to go,” is quite priceless.

If you want to dive deeper into this topic, join Erich's keynote, "Insights Gained From Developing an In-House Library," at the Digit Conference on October 4!