You should treat your Microservices as if they are Distributed Plugins
Are Microservices just Plugins in disguise?
Microservice Architecture, as defined by Medium engineering team, resides on three design principles:
In microservice architecture, multiple loosely coupled services work together. Each service focuses on a single purpose and has a high cohesion of related behaviors and data.
The core design principles are:
- Single Responsibility¹ — each service must have just one purpose i.e. single reason to exist
- High Cohesion — each service must have everything needed for the job and prevent abstractions leaks
- Loose Coupling — each service must be unaware of all the others
But we already have another Architectural Pattern residing on the same three design principles: Plugins² (also referred as Microkernel Architecture).
There are a lot of similarities between the two patterns but there is also one crucial difference. Software industry knows a lot of examples of successfully implemented plugin architectures. In this article we will see what knowledge and experience can be salvaged from these examples and how we can reuse it to improve microservices.
Both microservices and plugins are not architectures by themselves but rather Architectural Patterns. By “Microservice Architecture” I mean an architecture that adopts microservices to its advantage. Same goes for plugins.
There’s a popular misconception that a microservice must do just one job. This is not always possible and shouldn’t be your primary goal when designing microservices. Let’s say a microservice uses its own database and no other microservice can read or write to it. In this case one job is to read data from this database and the other job is to write data to it. These are two different functions and both must be performed by the same microservice as nobody else has access to this database.
A well designed microservice must rather have a single purpose, for example to manage (read and update) user shopping cart.
A function must do just one job but a microservice comprises several functions and thus it can do multiple jobs.
There are many similarities between the two patterns:
Each microservice or plugin has its own lifecycle meaning that it can be designed, developed, versioned, released, and decommissioned independently from the rest of the system.
Different microservices or plugins can be implemented by different teams perusing orthogonal goals. These teams might not even know about each other existence. This is only possible if microservices and plugins are loosely coupled and don’t know about each other.
Independent choice of technologies
Each microservice or plugin can be developed using its own set of technologies, such as programming languages and databases, best suited for the purpose.
This commonality is obvious for plugins. In fact, a plugin could be developed just to work with some exotic data store. It is less obvious but still true for microservices. Common mistake in this case would be to share a common source of truth among several microservices. For example, it might be decided to do so in order to keep these microservices stateless, or to run some cross-cutting functionality, like audit trails, through stored data of all these microservices. It would violate both loose coupling (microservices can interfere with each other avoiding microservices’ protocols) and high cohesion (data structure is no longer well-encapsulated).
Microservices and plugins typically assume existence of a common coordinator. In case of microservices this role is partially played by a cloud provider infrastructure, Kubernetes etc. In case of plugins this role is played by a microkernel.
Service Discovery and Marketplace
Microservices and plugins architectures typically rely on service discovery which is implemented through some common registry. A folder in a file system is the most primitive example of such registry for plugins.
In both cases one microservice or plugin must not assume that some other one is discoverable and reachable. Even worse, microservices must not perform service discovery themselves through configuration but rather rely on coordinator.
In case of plugins it is also considered a good practice to have a Marketplace where independent developers can publish their plugins. For example, modern web browsers typically have it. From microservices perspective it is conceivable that different tenants of the same application can run different sets of microservices as different clients want different functionality.
Apache Maven combines plugins discovery and central repository into one piece of functionality. You can run any command of any existing plugin as all existing plugins are discoverable through central repository.
Microservices and plugins are free to use any technologies of their choice but it doesn’t apply to User Interface, so choose UI technology wisely.
This approach agrees with Micro Frontends³ pattern where different parts of UI are implemented independently from each other, but the whole UI is still implemented using the same technology, for example HTML with React.
Plugins also have very limited control over UI technological stack. The most known example here would be any modern web browser allowing plugins to interfere with its own UI as well as visited web pages, but UI technology is still HTML, the same for all plugins.
It is common in plugin architecture to have a rigid workflow which every plugin must obey.
In Apache Maven this workflow is called “lifecycle”. There are several well defined unalterable lifecycles and every plugin can perform any action as long as this action fits to some step in one of these lifecycles.
It is less common but still conceivable to define a rigid workflow in microservices architecture and require all microservices to obey it. One example would be if all microservices are implemented as middleware serving different parts of incoming requests. This approach agrees with Event-Sourcing⁴ pattern.
A typical plugin architecture on NoUML diagram would look like this:
A typical microservice architecture would look messier with dependencies and control flows going between individual microservices. It basically means that industry standards for Low Coupling and High Cohesion restrictions are considered to be weaker for microservices compared to plugins.
There’s just one fundamental difference between Microservices and Plugins:
Microservices are distributed
Microservices are always a part of a larger distributed system whereas plugins are typically deployed on the same host often as dynamic modules inside the same process (even though for stability purposes modern applications prefer to run every plugin in a dedicated process). As such microservices are prone to all terrors of Distributed Systems design:
- The network is not reliable and prone to all sorts of partitioning. A microservice can become unreachable at any time without even realizing it. Health monitoring precautions must be in place making sure that network partitioning doesn’t cause devastating consequences for the whole system.
- Latency is not zero, Bandwidth is not infinite and Transport is costly. Plugins can have arbitrarily intricate communication protocols whereas microservices protocols must minimize the number of roundtrips as well as the volume of the payload. The protocol must also take Network Topology into account handling the case where some communicating microservices can be located in the same Placement Group very close to each other and some can be located in different Geographical Regions.
- Fine-grained Scalability is one of the desired Quality Attributes for microservices whereas in case of plugins it is typically enough to run just one instance of every plugin. This means that several instances of the same microservice must have one of the following properties:
◦ have common source of truth. In this case the problem of scalability is just shifted from microservices to the source of truth which might be acceptable if the source of truth is supplied by a cloud provider and “infinitely scalable” (meaning that you can scale it as much as scaling cost allows you).
◦ serve different partitions of the system. In this case the problem of scalability is resolved by partition algorithm one tier above microservices.
◦ run some consensus algorithm.
Breaking the Monolith
Taking all of it into account there’s a natural way of breaking the monolith⁵: the former monolith must also serve as a microkernel gradually shaving off distributed plugin-like microservices.
The monolith must define the rigid workflow while microservices must take away domain-related purposes out of it, leaving it with only coordination-related ones.
During the initial phase the future microservices could be just plugins of the monolith running inside the same process. The intermediate goal is to isolate single purpose, high cohesion, low coupling pieces of functionality and only then spread and scale these independent nuggets across distributed infrastructure.
¹ Robert C. Martin. Clean Architecture, 2018, p 62.
² David Rice, Matt Foemmel. Plugin; Patterns of Enterprise Application Architecture by Martin Fowler, p 499.
³ Cam Jackson. Micro Frontends, 2019.
⁴ Martin Fowler. What do you mean by “Event-Driven”? 2017.
⁵ Zhamak Dehghani. How to break a Monolith into Microservices, 2018.