microservices antipatterns and pitfalls - The “I Was Taught to Share” AntiPattern
microservice is known as a “share-nothing” architecture. Pragmatically, I prefer to think of it as a “share-as-little-as-possible” architecture because there will always be some level of code that is shared between microservices. For example, rather than having a security service that is responsible for authentication and authorization, you might have the source code and security functionality wrapped in a JAR file named security.jar that all services use. Assuming security is handled at the services level, this is generally a good practice because it eliminates the need to make a remote call to a security service for every request, thereby increasing both performance and reliability.
However, taken too far, you end up with a dependency nightmare as illustrated in Figure 3-1, where every service is dependent on multiple custom shared libraries.
Figure 3-1. Sharing multiple custom libraries
This level of sharing not only breaks down the bounded context of each service, but also introduces several issues, including overall reliability, change control, testability, and deployment.
Too Many Dependencies
If you consider how most object-oriented software applications are developed, it’s not hard to see the issues with sharing, particularly when migrating from a monolithic layered architecture to a microservices one. One of the things to strive for in most monolithic applications is code reuse and sharing. Figure 3-2 illustrates the two main artifacts (abstract classes and shared utilities) that end up being shared in most monolithic layered architectures.
Figure 3-2. Sharing inheritance structures and utility classes
While creating abstract classes and interfaces is a common practice with most object-oriented programming languages, they get in the way when trying to migrate modules to a microservices architecture. The same goes with custom shared classes and utilities such as common date or string utilities and calculation utilities. What do you do with the code that needs to be shared by potentially hundreds of services?
One of the primary goals of the microservices architecture style is to share as little as possible. This helps preserve the bounded context of each service, which is what gives you the ability to do quick testing and deployment. With microservices it all boils down to change control and dependencies. The more dependencies you have between services, the harder it is to isolate service changes, making it difficult to separately test and deploy individual services. Sharing too much creates too many dependencies between services, resulting in brittle systems that are very difficult to test and deploy.
Techniques for Sharing Code
It’s easy to say the best way to avoid this antipattern is simply not to share code between services. But, as I stated at the start of this chapter, pragmatically there will always be some code that needs to be shared. Where should that shared code go?
Figure 3-3 illustrates the four basic techniques for addressing the problem of code sharing: shared projects, shared libraries, replication, and service consolidation.
Figure 3-3. Module-sharing techniques
Using a shared project forms a compile-time binding between common source code that is located in a shared project and each service project. While this makes it easy to change and develop software, it is my least favorite sharing technique because it causes potential issues and surprises during runtime, making applications less robust. The main issue with the shared project technique is that of communication and control—it is difficult to know what shared modules changed and why, and also hard to control whether you want that particular change or not. Imagine being ready to release your microservice just to find out someone made a breaking change to a shared module, requiring you to change and retest your code prior to deployment.
A better approach if you have to share code is to use a shared library (e.g., .NET assembly or JAR file). This approach makes development more difficult because for each change made to a module in a shared library, the developer must first create the library, then restart the service, and then retest. However, the advantage of the shared library technique is that libraries can be versioned, providing better control over the deployment and runtime behavior of a service. If a change is made to a shared library and versioned, the service owner can make decisions about when to incorporate that change.
A third technique that is common in a microservices architecture is to violate the don’t-repeat-yourself (DRY) principle and replicate the shared module across all services needing that particular functionality. While the replication technique may seem risky, it avoids dependency sharing and preserves the bounded context of a service. Problems arise with this technique when the replicated module needs to be changed, particularly for a defect. In this case all services need to change. Therefore, this technique is only really useful for very stable shared modules that have little or no change.
A fourth technique that is sometimes possible is to use service consolidation. Let’s say two or three services are all sharing some common code, and those common modules frequently change. Since all of the services must be tested and deployed with the common module change anyway, you might as well just consolidate the functionality into a single service, thereby removing the dependent library.
One word of advice regarding shared libraries—avoid combining all of your shared code into a single shared library like common.jar. Using a common library makes it difficult to know whether you need to incorporate the shared code and when. A better technique is to separate your shared libraries into ones that have context. For example, create context-based libraries like security.jar, persistence.jar, dateutils.jar, and so on. This separates code that doesn’t change often from code that changes frequently, making it easier to determine whether or not to incorporate the change right away and what the context of the change was.