1/3 Polyglot microservice development with Dapr – What are polyglot Microservices?
This blog post series about polyglot microservices is based on my master’s thesis on “Polyglot Microservice Development with the Distributed Application Runtime”, which I have written in cooperation with white duck. The aim of this series is to give you an overview of polyglot microservices and show you why modern tooling like Dapr is needed to develop polyglot microservices efficiently. However, the scope of the blog posts is not limited to Dapr, but attempts to give you a holistic overview of the topic and the tooling options available.
In the first part of the series, you will get an overview of polyglot microservices, how they differ from single language microservices, what is necessary to implement them and their advantages and disadvantages. After that, the second part uses the results of expert interviews conducted as part of my master’s thesis to show you the challenges that arise when using polyglot microservices. Lastly, the third part will provide you with an overview of the options for dealing with these challenges and how you can benefit from the advantages of polyglot microservices by leveraging modern tooling.
- Polyglot microservices
- Distributed Systems
- Necessary non-functional requirements
- Single language vs polyglot microservices
- Advantages of polyglot microservices
- Disadvantages of polyglot microservices
Polyglot or multi-language microservices are a microservice architecture in which developers use multiple programming languages and technologies simultaneously. Therefore, each microservice can use the technology that is best suited for implementing the microservice’s use case. This also means developers can choose the programming language they know the best or love the most. However, polyglot microservices are not a new programming or architectural style, but a variant of microservice architecture.
They are the application of free technology choice that is often touted as one of the key benefits of the microservice architecture. For this reason, single language and polyglot microservice architectures have the same fundamentals and requirements. Both are distributed systems, and both require the implementation of non-functional requirements that are necessary to architect a microservice architecture.
In contrast to a monolith, microservices only represent individual functions of a system and therefore only form a complete system when they interact. Consequently, microservices are distributed systems and thus must implement several non-functional requirements. In this way, microservices trade off the advantages of easier development of individual components, better scalability, and faster deployment, against the challenges associated with distributed systems. These are famously known as the eight fallacies of distributed systems. To address these fallacies, multiple non-functional requirements must be implemented in each microservice.
Necessary non-functional requirements
Which non-functional requirements are necessary for a microservice varies from microservice to microservice. But all microservices that use the network must address potential network failures and be able to communicate over the network. And as we just saw, all microservices need to communicate with each other to form a system, thus using the network. Also, in most cases, a microservice architecture will need state management capabilities at some point. Furthermore, each microservice must have observability capabilities, such as logging, monitoring, and tracing of requests. Although observability is not essential for a microservice to function, without it, it will be difficult to operate and debug microservices.
The non-functional requirement service invocation represents one of the fundamental requirements of a microservice architecture. As a distributed system, microservices must invoke each other over the network, thus implementing service invocation capability. For this to be possible, microservices must first have information about the address at which other microservices can be reached. To avoid having to store this information in the application code and thus create dependencies between microservices, a service discovery should be used. Such a service discovery provides a database that has information about all instances, addresses, and the availability of microservices in the system.
This feature can be implemented in two variants: client-side service discovery and server-side service discovery. Both variants have their advantages and disadvantages, but both require either the implementation of logic in each microservice or an additional network component for service discovery. Additionally, a service registry is required to provide information about instances and locations of microservices for either client-side or server-side service discovery implementations.
The non-functional requirement asynchronous communication is often implemented with the publish-subscribe pattern. Asynchronous communication is also a type of service invocation, but with the additional requirements, that asynchrony introduces. The publish-subscribe pattern is thereby used to implement a loosely coupled microservice architecture and efficient messaging between microservices. An alternative is the request-reply pattern, which has the disadvantage of creating a tight coupling between microservices. Therefore, the publish-subscribe pattern should be preferred for implementing asynchronous communication.
Although asynchronous communication is more efficient than synchronous communication and enables loosely coupled microservices, it is also more complex to implement. To implement the publish-subscribe pattern, either a third-party message broker can be used, or a message broker can be implemented by the developer. In both cases, it is necessary to integrate microservices with the message broker, which requires either a programming language-specific library or custom code. Consequently, implementing the non-functional requirement asynchronous communication with the publish-subscribe pattern requires an implementation in each microservice that uses it.
The implementation of the non-functional requirement state management is necessary, when microservices are required to retrieve or persist data. While it may not be necessary to implement state management in every microservice, most services will need to store data at some point. Additionally, to prevent tight coupling of microservices, each microservice should have its own state management.
In the context of a distributed system, this means distributed databases with their own set of challenges. For example, data consistency can no longer be achieved simply by using database transactions in a single database. Therefore, new concepts such as eventual consistency or the saga pattern are required, which developers may not be familiar with. In addition, distributed databases mean that multiple microservices must be integrated with a database, which is usually a third-party component with its own API and integration libraries.
As distributed systems, microservices communicate over the network, thus the non-functional requirement security must be considered. Unlike a monolith that uses inter-process communication, network calls are much more vulnerable to attack and must be protected. This means that each microservice must implement security, while a monolith only needs to implement network security capabilities when it communicates with other systems. Of course, implementing authentication, authorization, and encryption in each microservice is very time-consuming. Therefore, in practice, there is often a gateway that secures the communication of microservices with other systems. This is known as the gateway offloading pattern. However, a gateway only ensures that all communication with external systems is secured, but not calls between microservices. Consequently, a trade-off must be made between safety and time for implementation.
As mentioned earlier, microservices are distributed systems that need to communicate over the network. Therefore, microservices must deal with network errors, network latency, lost messages, and unreachable microservices on which they rely. This means that implementing the non-functional requirement resilience is not optional, but a necessity in a microservice architecture. Consequently, each microservice must take measures for network outages by using mechanisms and patterns such as retries, rate limiting, throttling, circuit breaker or bulkhead.
Unfortunately, it is necessary to implement these measures in many places across microservices, and to make it worse implementation is not straightforward. Of course, it is possible to use libraries for these patterns, but this in turn ties the developer to specific programming languages or requires using different libraries for each programming language. In summary, these patterns enable the implementation of robust microservice architectures that can handle network-related failures. But they come with a price tag and once again require a trade-off to be made.
The non-functional requirement observability is not necessary for a microservice architecture to function. But without it, eventually, it will be difficult to debug and operate a microservice architecture. Therefore, to avoid these problems, observability should be implemented from the start. Observability can be divided into three pillars: Distributed Tracing, Logging and Monitoring.
In this definition, distributed tracing is about correlating logs generated in different and distributed microservices to debug service calls across multiple services. Furthermore, logging is about creating and storing events for later analysis. Finally, monitoring is about gaining insight into the state of the infrastructure on which applications and services are running. In the context of microservices, the challenge lies in aggregating and correlating the distributed data generated by observability implementations. Again, this is a requirement that can be solved by using third-party libraries and components. But it does not come for free and must be considered in every microservice.
Single language vs polyglot microservices
As we can see, there is a lot to implement and to consider when using a microservice architecture. This applies to both single language and polyglot microservices. The difference lies in the number of programming languages in which non-functional requirements must be implemented.
Since each programming language has its own peculiarities, and tools are usually not available in every programming language, the complexity and the number of implementations increases with each additional programming language. On the other hand, using more than one technology stack has also various advantages and might be even required to implement a use case. Thus, choosing between single language and polyglot microservices is not straightforward and should be done carefully.
Advantages of polyglot microservices
- Each microservice can use the optimal technology for its use case (e.g. Python for AI/ML, Rust for Safety and Speed, etc.)
- Easier adoption of new and innovative technology, which is often only available in certain programming languages
- Developers can use the programming language they love the most
- Developers are more flexible about which projects they can contribute to
Disadvantages of polyglot microservices
- Differences in programming languages
- Differences in tooling (libraries, SDKs, frameworks)
- Sharing of implementations between different programming languages is difficult
- Time for implementation and complexity are increasing with each additional programming language
While it may seem that using polyglot microservices is out of the question after we have looked at the complexity that even single language microservices have to deal with, there are significant advantages in using them. The concise list of benefits above may not fully reflect this, but just being able to use the optimal technology for each use case and microservice in a microservice architecture can be a huge advantage.
Before we look at modern tooling such as Dapr that allows us to take advantage of polyglot microservices without the complexity overhead, we’ll need to take a closer look at the drawbacks. This is necessary to understand what challenges modern tooling needs to solve to reduce the additional complexity of polyglot microservices. Therefore, the next blog post in this series will show you what challenges experienced developers face with polyglot microservices. Stay tuned!