The pros and cons of monoliths vs microservices
It’s a common story: the product team gets early success and grows into a large monolithic code base. While everything is in a single code base, features can be added quickly. This is partly due to the ability to leverage shared code across each feature in the codebase.
When your team is adding a new feature, a developer can leverage the existing codebase for needs such as logging or special error handling. This gives developers more time to focus on writing the code that brings immediate value for the end-user.
The tricky part comes as the product and organization continue to grow. With growth, it becomes cumbersome to add new features to the codebase. Soon there are too many developers working on the same code, and, with so many concurrent changes, coordinating releases gets more difficult. Faced with this issue, many organizations decide to break up the monolith into microservices.
Breaking up the monolith into microservices
While it enables more developers to work independently from each other, breaking things up introduces new obstacles. One new problem is the additional complexity and unreliability of network calls between services.
This complexity also slows down development. It creates the need to resolve the same types of problems every time a new service is added to the architecture. The service can no longer just call previously written functions from other features.
Now the team has to decide how to handle the situation.
What third-party libraries should you use? What’s the right interface? How much should be tested? This is a cost incurred by every team that is forced to spend time implementing redundant solutions to cross-cutting concerns instead of solving the primary issues for that service.
One possible solution is to use common internal shared libraries across microservices. This approach enables teams to share solutions to common problems while retaining the autonomy to use custom solutions where needed.
At CircleCI we develop and manage these libraries much like third-party libraries (e.g. each release is versioned and services declare a dependency on their preferred version).
Examples and benefits of shared libraries
There are cases where it makes sense to solve problems in a single place rather than have separate, disparate solutions.
Shared logging library
While it’s easy to get logging working with only the standard library in every language, teams can benefit from a common library that takes care of the boilerplate for common logging needs. This includes features like configuration for forwarding, rotation, formatting, and secrets redaction.
Shared tracing library
Tracing is a similar example. There are meaningful gains to be made with observability once each service is exporting traces, like being able to observe every service that was touched in a single customer request.
Like logging, teams benefit from having tracing work out of the box rather than being forced to configure a third-party library the same way. The library can also include logic that is particular to your organization. You might have certain fields (e.g. customer-id, api-version) that are meaningful but require every service to follow the same approach for tagging traces.
Shared dependency conflict management library
Finally, another case is keeping on top of third-party dependencies. This can include bug fixes, performance improvements, or security patches.
It’s relatively simple for each service to notice when the version of their JSON parsing library has a critical security update that needs patching. It can be more challenging, though, for every service to know if a particular version is compatible with the other libraries being used by the service for things such as logging and tracing. Having these dependencies managed in a single place reduces the steps teams need to take to secure their code.
At CircleCI, our solution to this problem is called clj-parent. In effect, clj-parent acts as a shared and versioned dependency lock file across our various services. It enables us to resolve those version conflicts in one place, so that teams simply bump its single version to get the proper dependency versions.
Downsides to shared libraries
No solution is perfect, and shared libraries have their own challenges.
Our clj-parent project introduces additional overhead to teams that need a new version of a managed dependency. We need to update the dependency version in clj-parent and publish that before a team can bring it into their build. If we are slow updating clj-parent, that team is now blocked waiting for it.
Shared libraries also take more upfront effort to get right. Adding new features requires getting consensus from various teams on the approach. Testing and good documentation are critical to ensure that it’s ready to be consumed by multiple teams. Finally, maintaining backwards compatibility is necessary to prevent breaking services using old versions.
Tips for implementing shared libraries
As Sam Newman describes in building microservices, it’s important not to introduce unnecessary coupling between microservices. Shared libraries should provide cross-cutting infrastructure rather than service domain logic or product functionality.
For the CircleCI, the solution to the update bottleneck challenge is to have a dedicated owner for the shared libraries. Having a dedicated team has ensured that the libraries are well-maintained and updated promptly when things like new CVEs arise.
More importantly, having an independent team provides an important checkpoint to ensure that the code being proposed for a library has benefits to more than one service. These libraries should be used to consolidate rather than collect functionality. In other words, we are standardizing on a common solution rather than putting various team’s solutions in one place.
It may not be feasible for your organization to have a team dedicated to building shared libraries. In this case, it’s important to have a library mindset to be cautious about what goes into your shared libraries.
Reduce the cost of building and maintaining
The benefits and trade-offs of monoliths vs microservices continue to be debated. At CircleCI we’ve attempted to break out services with well-defined domains. In doing so, a set of shared libraries for concerns like logging, tracing, and metrics have helped reduce the cost of building and maintaining those new services.
You can get started reducing costs and increasing efficiencies right away, by signing up for your CircleCI free trial today.