DEV Community

Cover image for 3 methods for microservice communication
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

3 methods for microservice communication

Written by Kyle Galbraith✏️

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.

LogRocket Free Trial Banner

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
                })
            }
        })
}
Enter fullscreen mode Exit fullscreen mode

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
                })
            }
        })
}
Enter fullscreen mode Exit fullscreen mode

The change is subtle. Now, instead of ServiceB returning a saved property, it is returning a statusUrl. 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 ServiceA can push messages to an SNS topic that ServiceB 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
        })
}
Enter fullscreen mode Exit fullscreen mode

ServiceB 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 ServiceA no longer receives a status URL to check on progress. This is because we only know that the message has been sent, not that ServiceB has received it.

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

Take note that there is still some coupling between the two services using this pattern. For instance, ServiceB and ServiceA 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
        })
}
Enter fullscreen mode Exit fullscreen mode

Notice here that our SNS topic message is a simple event 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.

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.

If you have any questions about this blog post, AWS, serverless, or coding in general, feel free to ping me via twitter @kylegalbraith. Also check out my weekly Learn by Doing newsletter or my Learn AWS By Using It course to learn even more about the cloud, coding, and DevOps.


Editor's note: Seeing something wrong with this post? You can find the correct version here.

Plug: LogRocket, a DVR for web apps

 
LogRocket Dashboard Free Trial Banner
 
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
Try it for free.


The post 3 methods for microservice communication appeared first on LogRocket Blog.

Top comments (0)