Good developers follow the principle of Don’t Repeat Yourself (DRY) when writing code. When faced with duplication, they refactor it to be shared—increasing the code quality and maintainability.
I have found that in the microservices world, the simplest way to reuse code is in the form of an external library. Use of such a library, however, is contradictory to the aim in microservices development to maintain autonomy (and development/deployment independence). If a change of code is required in the shared library, this may result in multiple microservices requiring deployment at the same time to ensure operational integrity. This inhibits their ability to vary independently. With these two competing principles, both aimed at maximizing maintainability, the proper approach is clearly somewhere in the middle and the correct answer depends on the situation. Certainly, if there is common code that is not expected to change over time and there is little drawback to sharing this code rather than duplicating it, then a shared library should be strongly considered.
Perspectives on DRY and Microservices
To illustrate how DRY and autonomy are at odds, take two microservices, A and B, that have identical source code z. The autonomy of A and B can be maximized by duplicating z across both microservices. As illustrated in dark blue above, this allows z to evolve to z’ and beyond in A without impacting B. In fact, the underlying programming language used in A can change without B having to know or care – this is one of the many advantages of microservices architecture and is especially useful when the microservices are maintained by independent teams.
If z is shared instead, a change for A in z will result in B changing and may require that A and B both be deployed at the same time. This could be the case for a change on a service boundary. That is, changes in z for A can mandate a change in B, thus reducing B’s independence from A.
This is a simplified, conceptual illustration of the problem and those of us deeper in development understand that this really depends on what ‘z’ is specifically. We also understand that there are ways to navigate the situation, like library versioning. But one thing is clear at this level; the overhead of managing change in z is higher when it is in a shared library. However, if we can be sure that z is going to be stable, and the value of reuse across microservices outweighs the cost, then sharing such code should be strongly considered.
Reviewing the available technical literature to see how the microservices community approaches sharing code, there seems to be early consensus that one should not apply DRY across microservices. For example, the use of client libraries that facilitate reuse of service model classes across microservices seems to be discouraged. This is echoed by Sam Newman in one of the leading books on microservice design: Building Microservices, in which he advocates against the use of such client libraries. When you look more closely at the reasoning, it comes down to difficulty in managing change. If the shared service model changes, then this may mandate synchronous deployment of those microservices that depend on it. Where such a service model is expected to change little (ie. known, stable domain), or change is relatively cheap (affected microservices owned by same team), the cost-benefit line between the principles of service autonomy and DRY shifts toward DRY.
When solving a business problem with software, it is important to always balance recognized software best practices against your business context and not just take them verbatim. If not, you may end up spending a lot of money on something that doesn’t benefit your business.
As an example, I’m reminded of the very early days of Java Enterprise Edition (back when it was called J2EE), when it was an accepted standard to use container-managed entity beans to house the business logic for domain entities. After a few years, the community found this approach too cumbersome and had to find alternative ways to approach modeling persistent entities. It’s from this painful experience that Java Persistence API (JPA) was born. In retrospect, businesses blindly adopting best practices of the time paid a relatively high cost in maintaining such code and pruning out their entity beans if/when they could afford to. Deeper scrutiny may have prevented such waste.
In a nutshell, by evaluating the industry best practices for microservices against the specific needs of the problem at hand, we may justify a unique take on where to draw the line when sharing code across microservices.
Drawing the Line
One of the most compelling places for code sharing is the service model, which by definition crosses at least 2 microservices (code required to send the request and that required of the consuming microservice to receive it). To illustrate the point, here are some specific code sharing examples:
Example 1: Shared Service Data
Imagine account data passed between many microservices for various types of processing, and core account data that is shared across all microservices. If code can’t be shared across microservices, this means that code is duplicated to model, store, marshall and unmarshall the data at a minimum. As is the case with duplicate code, it can (and will) vary independently, causing service breakage for backward incompatible changes and difficulties ahead in trying to reconcile common changes to be applied to differing code bases. It’s worth noting that if many microservices need to access the model in the same way (ie. the exact same account model needs to be used in 3 or more services), the separation of concerns for these microservices may not be quite right, but in the presence of competing factors that don’t allow a different service decomposition, a shared library is a strong option.
Example 2: Shared Data Types
Once upon a time, Java did not natively support currency. If we couldn`t apply DRY across microservices, we would have to author a subset of the equivalent of java.util.Currency for every single microservice and likely work at length to ensure that the code copies were in synch to prevent costly bugs when, ultimately, there is a large core of currency that should always be the same across the enterprise. The same concern applies for common domain types across microservices.
Sharing established service model code and data types across microservices not only has the benefit of less code, it also helps to ensure a ubiquitous language for the business and developers. Again, this is especially powerful when these concepts are largely unchanging for a business.
Cautionary note on shared behavior
As we all know, in object oriented programming, classes are a combination of state and behavior and although the sharing of state may be a good idea, caution should be taken when sharing behavior in a microservices environment.
Example: Derived Fields
A simple example of this danger is if there was a derived field on the shared service model and not all microservices had deployed a bug fix in how this data was derived. This would mean that the same field would have different values depending on the microservice called—quite a dangerous prospect, especially when dealing with financial data.
If a requirement presents itself to share behavior, one should always try to encapsulate it behind the bounds of a microservice.
As with all best practices, the idea that one should not apply DRY across microservices should be taken with caution, and measured against the unique business needs and landscape of a given software initiative.