Microservices Architecture is a well-known pattern for building a complex system that consists of loosely coupled modules. It provides better scalability, and it is easier to develop the system in multiple teams so that they don’t interfere with each other too much. Moreover, microservices can be platform-agnostic and be written in different languages. However, it is important to choose the right way of communication between them. Otherwise, this kind of architecture can do more harm than good.
There are 3 main ways to organize interaction between microservices:
Remote Procedure Calls (RPC)
Representational State Transfer (REST)
All three ways have different pros and cons in different situations.
Remote Procedure Calls
RPC allow us to hide the complexity of making a call to a remote service and make an operation look like it is executing locally. There are a lot of frameworks that help to make such calls. For example, the WCF framework in .NET helps to easily generate client-code so that a call to a remote service occurs almost seamlessly. Also, it supports different formats of passing data, including the binary format which allows achieving high performance.
Nevertheless, the RPC approach has some disadvantages. First of all, some RPC frameworks are technology-specific and require that both client and server run on the same platform like Java or .NET. Such restrictions in interoperability can limit our options when we develop our architecture.
Another disadvantage is that RPC may require updating both the client and the service whenever some insignificant part of the contract changes. For example, if we want to remove the unnecessary field of the object that is returned by the service, we might be required to regenerate the client-side code. Otherwise, it could break when we deploy the new version of our service. Such a problem often occurs when the data is being passed in binary form.
REST is another popular style of communication. In fact, it is usually a default choice when it comes to building microservices because it provides scalability and flexibility. Also, it makes the interface predictable and consistent if you follow the standards closely. For example, if you use the POST request method, you know that some entity will be created on the server and if you use the GET method, you will receive a representation of an existing entity. No need to come up with your own custom names for operations like “createOrder” or “retrieveOrder”.
One of the most important concepts of REST has a strange abbreviation HATEOAS (Hypermedia as the Engine of Application State). Basically, it helps us to decouple the interface of the operation from the particular implementation details. For example, if our consumer received a representation of an object Photo from the service, we might also want to find out in which album this photo is situated. If we don’t follow REST, we need to know in advance which URL address we should hit to get the album, for example
/albumStorage/id. But what if the location of the album changes to another URL? We would need to update the configuration of our consumer to reflect that change so that we don’t send a request to the old address. This is time-consuming.
HATEOAS comes in handy here. According to Wikipedia this REST principle states the following:
A REST client needs little to no prior knowledge about how to interact with an application or server beyond a generic understanding of hypermedia.
In our example, if we follow the REST approach then when a consumer receives a Photo from the service, the JSON response will contain a separate field “album” with a value that represents a location of this resource:
/albums/id. The client just needs to know that if it wants to get an album it should look in the corresponding field in the response. No need to know the exact URL in advance. Therefore if the URL of the album changes in the future, we don’t need to update the client.
There are some situations when REST is not optimal though. For example, if performance is the primary concern, it might be better to try other options. This is because REST works via HTTP protocol which has some overhead in terms of passing data. If you want to increase the performance as much as possible, you could consider using RPC frameworks that work with other networking protocols, like UDP.
Though REST-style is probably the best default choice for inter-services communication, there is another approach that helps to decouple microservices even more — event messaging.
Consider a situation when we have to perform a business operation like making a purchase. In case of a pure REST-approach, our PurchaseService will make a call to the NotificationService to send an email to a customer with the details about the purchase. But what if the NotificationService has a bug in it and it takes forever to process the request? The PurchaseService will be frozen until the NotificationService responds and this will spoil the customer experience — nobody wants to wait too long until our purchase is processed.
We can resolve this problem by applying events instead of requests/responses. Since immediate sending of an email notification is not really important at the time of purchase, it is ok if it is sent several minutes later. We just need to make sure that this event is scheduled and an email will be sent eventually. PurchaseService actually doesn’t need to know whether NotificationService is operational at the time of the purchase, so it can just emit an event that will be stored in the event message broker (like RabbitMQ for example).
The NotificationService will subscribe for the events in the broker and will take them from the event queue. As a result, we don’t have a tight coupling between PurchaseService and NotificationService and they can work independently. Once the event is stored in the events queue of the broker, PurchaseService will return a response to a client, and customer experience will be improved because there is no need to wait until all non-urgent steps are finished.
Though event-based communication style has a lot of advantages, they come with some tradeoffs. It may be hard to understand all steps of the business process since each service works independently and there is no single service that is responsible for the overall operation. This may lead to strange bugs when one part of the process finished correctly and another — failed. Debugging can be difficult since it is hard to say which exact request in microservice A caused the wrong behavior in microservice B. This problem can be mitigated by using correlation IDs and monitoring tools which can bind all parts of the operation together and show some meaningful logs.
As we can see, all 3 communication styles have their own pros and cons. There is no silver bullet, it is ok to use a mix of them, we just need to make sure that we use them in the right context. I recommend to use REST by default and RPC for types of operations that require extremely low latency. In case some operations are non-urgent and don’t require immediate feedback from the service, an event-based approach is a way to go.
I hope this information was useful and cast some light upon the topic. For further reading, I recommend the following great books: “Building Microservices” by Sam Newman, “Microservices Patterns” by Chris Richardson.