how-microservices-communicate-with-each-other

How Microservices Communicate with Each Other

There are three  methods for microservice communication

In the world of microservice architecture, we build out an application via a collection of services. Each service in the collection tends to meet the following criteria:

  • Loosely coupled
  • Maintainable and testable
  • Can be independently deployed

Each service in a microservice architecture solves a business problem in the application, or at least supports one. A single team is responsible and accountable for one or more services in the application.

Microservice architectures can unlock a number of different benefits.

  • They are often easier to build and maintain
  • Services are organized around business problems
  • They increase productivity and speed
  • They encourage autonomous, independent teams

These benefits are a big reason microservices are increasing in popularity. But potholes exist that can derail all these benefits. Hit those and you’ll get an architecture that amounts to nothing more than distributed technical debt.

Communication between microservices is one such pothole that can wreak havoc if not considered ahead of time.

The goal of this architecture is to create loosely coupled services, and communication plays a key role in achieving that. In this article, we are going to focus on three ways that services can communicate in a microservice architecture. Each one, as we are going to see, comes with its own benefits and tradeoffs.

HTTP communication

The outright leader when choosing how services will communicate with each other tends to be HTTP. In fact, we could make a case that all communication channels derive from this one. But, setting that aside, HTTP calls between services is a viable option for service-to-service communication.

It might look something like this if we have two services in our architecture. ServiceA  might process a request and call ServiceB  to get another piece of information.

function process(name: string): Promise<boolean> {
    /** do some ServiceA business logic
        ....
        ....
    */
    /**
     * call ServiceB to run some different business logic
    */
    return fetch('https://service-b.com/api/endpoint')
        .then((response) => {
            if (!response.ok) {
                throw new Error(response.statusText)
            } else {
                return response.json().then(({saved}) => {
                    return saved
                })
            }
        })
}

The code is self-explanatory and fits into the microservice architecture. ServiceA owns a piece of business logic. It runs its code and then calls over to ServiceB to run another piece of business logic. In this code, the first service is waiting for the second service to complete before it returns.

What we have here is synchronous HTTP calls between the two services. This is a viable communication pattern, but it does create coupling between the two services that we likely don’t need.

Another option in the HTTP spectrum is asynchronous HTTP between the two services. Here is what that might look like:

function asyncProcess(name: string): Promise<string> {
    /** do some ServiceA business logic
        ....
        ....
    */
    /**
     * call ServiceB to run some different business logic
    */
    return fetch('https://service-b.com/api/endpoint')
        .then((response) => {
            if (!response.ok) {
                throw new Error(response.statusText)
            } else {
                return response.json().then(({statusUrl}) => {
                    return statusUrl
                })
            }
        })
}

The change is subtle. Now, instead of <span class="typ">ServiceB</span> returning a <span class="pln">saved</span> property, it is returning a <span class="pln">statusUrl</span>. This means that this service is now taking the request from the first service and immediately returning a URL. This URL can be used to check on the progress of the request.

We have transformed the communication between the two services from synchronous to asynchronous. Now, the first service is no longer stuck waiting for the second service to complete before returning from its work.

With this approach, we keep the services isolated from one another, and the coupling is loose. The downside is that it creates extra HTTP requests on the second service; it is now going to be polled from the outside until the request is completed. This introduces complexity on the client as well since it now must check the progress of the request.

But asynchronous communication allows the services to remain loosely coupled from one another.

Message communication

Another communication pattern we can leverage in a microservice architecture is message-based communication.

Unlike HTTP communication, the services involved do not directly communicate with each other. Instead, the services push messages to a message broker that other services subscribe to. This eliminates a lot of complexity associated with HTTP communication.

It doesn’t require services to know how to talk to one another; it removes the need for services to call each other directly. Instead, all services know of a message broker, and they push messages to that broker. Other services can choose to subscribe to the messages in the broker that they care about.

If our application is in Amazon Web Services, we can use Simple Notification Service (SNS) as our message broker. Now <span class="typ">ServiceA</span> can push messages to an SNS topic that <span class="typ">ServiceB</span> listens on.

function asyncProcessMessage(name: string): Promise<string> {
    /** do some ServiceA business logic
        ....
        ....
    */
    /**
     * send message to SNS that ServiceB is listening on
    */
    let snsClient = new AWS.SNS()
    let params = {
        Message: JSON.stringify({
            'data': 'our message data'
        }),
        TopicArn: 'our-sns-topic-message-broker'
    }
    return snsClient.publish(params)
        .then((response) => {
            return response.MessageId
        })
}

<span class="typ">ServiceB</span> listens for messages on the SNS topic. When it receives one it cares about, it executes its business logic.

This introduces its own complexities. Notice that <span class="typ">ServiceA</span> no longer receives a status URL to check on progress. This is because we only know that the message has been sent, not that <span class="typ">ServiceB</span> has received it.

This could be solved in many different ways. One way is to return the <span class="typ">MessageId</span> to the caller. It can use that to query <span class="typ">ServiceB</span>, which will store the <span class="typ">MessageId</span> of the messages it has received.

Take note that there is still some coupling between the two services using this pattern. For instance, <span class="typ">ServiceB</span> and <span class="typ">ServiceA</span> must agree on what the message structure is and what it contains.

Event-driven communication

The final communication pattern we will visit in this post is the event-driven pattern. This is another asynchronous approach, and it looks to remove the coupling between services altogether.

Unlike the messaging pattern where the services must know of a common message structure, an event-driven approach doesn’t need this. Communication between services takes place via events that individual services produce.

A message broker is still needed here since individual services will write their events to it. But, unlike the message approach, the consuming services don’t need to know the details of the event; they react to the occurrence of the event, not the message the event may or may not deliver.

In formal terms, this is often referred to as “event only-driven communication.” Our code is like our messaging approach, but the event we push to SNS is generic.

function asyncProcessEvent(name: string): Promise<string> {
    /** do some ServiceA business logic
        ....
        ....
    */
    /**
     * call ServiceB to run some different business logic
    */
    let snsClient = new AWS.SNS()
    let params = {
        Message: JSON.stringify({
            'event': 'service-a-event'
        }),
        TopicArn: 'our-sns-topic-message-broker'
    }
    return snsClient.publish(params)
        .then((response) => {
            return response.MessageId
        })
}

Notice here that our SNS topic message is a simple <span class="kwd">event</span> property. Every service agrees to push events to the broker in this format, which keeps the communication loosely coupled. Services can listen to the events that they care about, and they know what logic to run in response to them.

This pattern keeps services loosely coupled as no payloads are included in the event. Each service in this approach reacts to the occurrence of an event to run its business logic. Here, we are sending events via an SNS topic. Other events could be used, such as file uploads or database row updates.

200’s only : Monitor failed and slow network requests in production

While implementing microservices is step one, making sure services continue to serve resources to your app in production is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRockethttps://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic Axios requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, and slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .

Conclusion

Are these all the communication patterns that are possible in a microservice-based architecture? Definitely not. There are more ways for services to communicate both in a synchronous and asynchronous pattern.

But, these three highlight the advantages and disadvantages of favoring synchronous versus asynchronous. There are coupling considerations to take into account when choosing one over the other, but there are also the development and debugging considerations to factor in as well.

This post is part of Microservices-Step by step”.

Back to home page

Leave a Reply

Your email address will not be published. Required fields are marked *