In this article, we will take a look at how the services within a system communicate with one another. We will cover various microservices inter-service communication strategies for either synchronous communication or asynchronous communication.
In previous my article, Software Architecture Patterns, we learned about monolithic and microservice based architectural patterns & designs and discussed their benefits and drawbacks. Also, in the Microservice Decomposition Services article, we looked at the various ways of decomposing a monolithic application into a microservice-based application, and we also built an application based on a microservice architecture. In my other article, Microservices Deployment Patterns, I have discussed various strategies for deploying microservice-based applications.
In a monolithic application, there is no need to inter-service communication or any remote call for internal business functions. The components invoke to another component by calling the language-level method or simple function calls.
But in case on the microservices-based application, all components might not be a part of a same service or machine, so, all components run in the several machines or on the clouds, and each service is typically a process, you have to call other processes or services by using various patterns that provide inter-communication between the services over the networks.
Approaches for Microservices Inter-Service Communication
In the Microservice architecture pattern, a distributed system is running on several different machines, and each service is a component or process of an enterprise application. That means these services at the multiple machines must handle requests from the clients of this enterprise application. Sometimes all these services collaborate to handle those requests. So all services interact using an inter-service communication mechanism.
But in case of the Monolithic application, all components are the part of the same application and run on the same machine. So, Monolithic application doesn’t require microservices inter-service communication mechanism.
Let’s see the following diagram about the microservices inter-service communication between service components of an application:
As you can see in the preceding diagram, a monolithic application has all of its components combined as a single artifact and deployed to a single machine. One component calls another using language-level method calls.
However, in the microservice architecture, all components of the application run on multiple machines as a process or service and they use inter-process communication to interact with each other.
In Microservice Architecture, we can classify our inter-service communication into two approaches like the following:
- Synchronous communication style
- Asynchronous communication style
Let’s have a look at these communication styles in detail.
Synchronous communication style
In this communication style, the client service expects a response within time and wait for a response by blocking a while. This style can be used by simple using HTTP protocol usually via REST. It is the simplest possible solution for microservices inter-service communication to interact with services. The client can make a REST call to interact with other services. The client sends a request to the server and waits for a response from the service (Mostly JSON over HTTP). For example, Spring Cloud Netflix provides the most common pattern for synchronous REST communication such as Feign or Hystrix.
In the above diagram, Order-Service calls Book-Service and waits for a response returned by Book-Service. Order-Service can then process Book-Service’s response in the same transaction that triggered the communication.
The synchronous communication approach does have some drawbacks, such as timeouts and strong coupling. For example, the Order Service needs to wait for the response from the Book Service, and the strong coupling means that the Order Service can’t work without the Book Service is available. We can avoid this coupling by using the Hystrix library, which enables us to use fallbacks in case the service is not available at that time.
There are numerous protocols, such as REST, gRPC, and Apache Thrift, that can be used to interact with services synchronously.
Synchronous one-to-one communication style
Most synchronous communications are one-to-one. In synchronous one-to-one communication, you can also use multiple instances of a service to scale the service. However, if you do, you have to use a load-balancing mechanism on the client side. Each service contains meta-information about all instances of the calling service. This information is provided by the service discovery server, an example of which is Netflix Eureka.
There are several load-balancing mechanisms you can use. One of these is Netflix Ribbon, which carries out load-balancing on the client side, as illustrated in the following diagram:
As you can see in the preceding diagram, we have multiple instances of a particular service, but the services are still communicating one-to-one. That means that each service communicates to an instance of another service. The load balancer chooses which method should be called. The following is a list of some of the most common load-balancing methods available:
- Round-Robin: This is the simplest method that routes requests across all the instances sequentially.
- Least Connections: This is a method in which the request goes to the instance that has the fewest number of connections at the time.
- Weighted Round-Robin: This is an algorithm that assigns a weight to each instance and forwards the connection according to this weight.
- IP Hash: This is a method that generates a unique hash key from the source IP address and determines which instance receives the request.
You can use the Spring Cloud, it provides support to Netflix libraries to balance the load on the client side; you can also use Spring’s RestTemplate or Feign client. Netflix Feign implements load-balancing internally. Let’s see the following code snippet:
@FeignClient(value = "ACCOUNT-SERVICE", fallback = AccountFallback.class) public interface AccountClient { @GetMapping("/accounts/customer/{customerId}") List getAccounts(@PathVariable("customerId") Integer customerId); } @Component public class AccountFallback implements AccountClient { @Override public List getAccounts(Integer customerId) { List acc = new ArrayList<>(); return acc; } }
In the preceding code, we also configured Hystrix as a circuit breaker pattern.
Asynchronous communication style
In this communication style, the client service doesn’t wait for the response coming from another service. So, the client doesn’t block a thread while it is waiting for a response from the server. Such type of communications is possible by using lightweight messaging brokers. The message producer service doesn’t wait for a response. It just generates a message and sends message to the broker, it waits for the only acknowledgement from the message broker to know the message has been received by a message broker or not. Let’s see in the following diagram:
As you can see in the preceding diagram, the Order Service generates a message to A Message Broker and then forgets about it. The Book Service that subscribes to a topic is fed with all the messages belonging to that topic. The services don’t need to know each other at all, they just need to know that messages of a certain type exist with a certain payload.
There are various tools to support lightweight messaging, you just choose one of the following message brokers that is delivering your messages to consumers running on respective microservices:
- RabbitMQ
- Apache Kafka
- Apache ActiveMQ
- NSQ
The above tools are based on the AMQP (Advanced Message Queuing Protocol). This protocol provides messaging based inter-service communication. Spring Cloud Stream also provides mechanisms for building message-driven microservices using either the RabbitMQ or the Apache Kafka.
So, let’s now look at two properties of the asynchronous communication style; they are as follows:
- Asynchronous one-to-one communication style
- Asynchronous one-to-many communication style
Let’s take a closer look at these properties.
Asynchronous one-to-one communication style
In this communication approach, each request of a service client is processed by one instance of a service. There are the following kinds of one-to-one interactions:
- Notification: In this case, a Client sends a request to the Service; the Client does not wait for a reply from that Service.
- Request/async response: Here, a Client sends a request to a Service, which replies asynchronously; the client does not block the thread while waiting and it is designed with the assumption that the response may not immediately arrive
The following diagram demonstrates one-to-one asynchronous service communication:
As you can see in the preceding diagram, each service has only one instance and the services are communicating through the message broker queue.
Asynchronous one-to-many communication style
In this communication approach, each request of a service client is processed by multiple service instances. There are the following kinds of one-to-many interactions:
- Publish/subscribe: In this approach, a Client publishes a notification message to the message broker and this notification message is consumed by zero or more interesting services.
- Publish/async responses: In this approach, a Client publishes a request message and then waits for a certain amount of time for responses from interested services
Take a look at the following diagram, which demonstrates one-to-many asynchronous service communication:
In the preceding diagram, you can see that there are multiple instances of each service. Here, a client service publishes a notification message as a topic and this topic is consumed by one or more instances of the interested services.
You can learn more about these microservices inter-service communication patterns, in Chris Richardson‘s blog, https://microservices.io/.