Like any other generic solution, the microservices architecture has its tradeoffs; some things become easier and some become harder. When switching to microservices, one of the most common challenges is the question of where to put shared code.
At first, it’s tempting to pull out all common code into separate libraries. The reasoning is simple: in programming we try to avoid code duplications. If you have the same code in two different files, you would usually try to extract it into a new function in a new file, so why not do the same on a higher level?
It’s not as simple as that. Shared libraries introduce hard dependencies between microservices, so they should be created with careful thought about the roles they’ll play across your entire set of services. I experienced this challenge first-hand before I joined Runnable.
For example, if you’ve abstracted your domain models in a library and you want to change the model, you would need to update all services that use that library at the same time. This involves updating code, passing reviews, tests, staging, etc. This makes your microservice architecture feel like a monolith.
So, does this mean that we shouldn’t ever use libraries with microservices? Not really. Philipp Hauer explains the exception in his post about the challenges of shared libraries and microservices:
It’s okay to have libraries for technical concerns (e.g. for logging or monitoring), because it’s not likely for a business requirement to affect these concerns.
This is a very important point. Microservices are usually composed of two categories of code:
Domain specific code. This code is specific to the problem that a given service tries to solve; i.e. all your domain models and business logic.
Support code (“technical concerns” in Philipp’s words). From my experience, a large part of microservices’ code is support code, so it’s natural that we should dedicate some time thinking about the best way of organizing it.
We have several libraries that we use at Runnable. Some were developed in-house, others are third-party code. Those libraries are our building blocks for any new microservices we develop.
Here is the list of those technical concerns with the specific libraries we use:
Monitoring (monitor dog) — Reports data to Datadog in a normalized way; e.g. prefixing each event with the service name.
Error handling (error cat) — Provides a error hierarchy that can be extended by each service and reports errors to Rollbar.
Configuration (loadenv) — Ensures that every microservice loads environment variables in the same way.
Worker server (ponos) — A common worker server with standard monitoring and error reporting built-in.
Docker/Swarm client (loki) — A Docker/Swarm client library with standard monitoring built-in.
Validation (joi) — A validation library for email addresses, dates, etc.
It’s important to be aware of when your code is a bad fit for a library. Here are few general rules of thumb:
If it contains business logic or domain-specific code — it shouldn’t be a library.
If it changes frequently based on new requirements — it shouldn’t be a library.
If it introduces coupling between consumers — it shouldn’t be a library.
Otherwise, go for it! Pulling out common code into separate libraries can speed up development of new services by allowing you to focus only on the specific problem your service needs to solve. Plus it can make monitoring, error handling, and managing configurations more consistent across all your modules.