avatarJoud W. Awad

Summary

The provided web content offers a comprehensive guide to communication styles in microservices architecture, emphasizing the importance of selecting appropriate synchronous and asynchronous communication strategies to optimize inter-service interactions.

Abstract

The article "A Guide to Communication Styles in Microservices Architecture" delves into the critical role of communication in microservices architectures. It outlines the two primary dimensions of interaction styles—one-to-one and one-to-many, and synchronous and asynchronous—highlighting the advantages and disadvantages of each. The post discusses various communication mechanisms, including REST and gRPC for synchronous remote procedure invocation, and asynchronous messaging patterns such as request/response, one-way notifications, publish/subscribe, and publish/async responses. It also addresses the challenges of message ordering, duplicate messages, and the use of message brokers, providing insights into the implementation of robust communication strategies that ensure system resilience, scalability, and maintainability. The article concludes by underscoring the significance of choosing the right communication style to meet the specific needs of a microservices application.

Opinions

  • The author suggests that understanding the nuances of different communication styles is essential before deciding on the most suitable communication mechanism for microservices.
  • REST APIs, while simple and widely used, have limitations such as difficulty in fetching multiple resources in a single request and challenges in mapping operations to HTTP verbs.
  • gRPC is presented as a compelling alternative to REST, offering a rich set of update operations and efficient, compact IPC mechanisms, but it may pose challenges for JavaScript clients and compatibility issues with older firewalls.
  • Asynchronous messaging is inherently more scalable and resilient, with the ability to decouple services and handle partial failures more gracefully.
  • The use of message brokers is advocated for their ability to buffer messages and provide location transparency, though the choice of broker should align with the application's specific messaging requirements.
  • The article emphasizes the importance of idempotency in message handlers and the need to track and discard duplicate messages to maintain data consistency and correct application behavior.
  • The author concludes that the selection of a communication style is pivotal for the performance, resilience, and evolution of a microservices architecture.
Communication Styles in Microservices Architecture

A Guide to Communication Styles in Microservices Architecture

Communication is a pivotal element in the architecture of microservices, with extensive discussions focusing on selecting the most effective methods for inter-service interactions. In this introductory blog post, we will explore and summarize the optimal communication strategies for microservices, providing insights into when and where to utilize each communication style effectively.

Interaction Styles

To effectively comprehend how services communicate within a microservices architecture, it is crucial to first familiarize oneself with the available interaction styles. Each style possesses its distinct advantages and disadvantages. Gaining a thorough understanding of these nuances is essential before making an informed decision on the most appropriate communication mechanism for your services. This foundational knowledge ensures that the selected method aligns well with the specific requirements and challenges of your system.

however, interaction styles can be categorized into two dimensions, the first dimension is whether the interaction is one-to-one or one-to-many:

  • One-to-one — Each client request is processed by exactly one service.
  • One-to-many — Each request is processed by multiple services.

The second dimension is whether the interaction is Synchronous or Asynchronous.

  • Synchronous — The client expects a timely response from the service and might even block while it waits.
  • Asynchronous — The client doesn’t block, and the response, if any, isn’t necessarily sent immediately.

the following Table shows the different Dimensions together

Communication Dimensions

let us have a brief talk about each.

One-To-One Interactions

  • Request/response — A service client makes a request to a service and waits for a response. The client expects the response to arrive in a timely fashion. It might even block while waiting. This is an interaction style that generally results in services being tightly coupled.
  • Asynchronous request/response — A service client sends a request to a service, which replies asynchronously. The client doesn’t block while waiting, because the service might not send the response for a long time.
  • One-way notifications — A service client sends a request to a service, but no reply is expected or sent.

One-To-Many Interactions

  • Publish/subscribe — A client publishes a notification message, which is consumed by zero or more interested services.
  • Publish/async responses — A client publishes a request message and then waits for a certain amount of time for responses from interested services.

Keep in mind that one service can have more than one way of communication

Communicating using the Synchronous Remote Procedure Invocation Pattern

A client sends a request to a service, and the service processes the request and sends back a response. Some clients may block waiting for a response, and others might have a reactive, non-blocking architecture. But unlike when using messaging, the client assumes that the response will arrive in a timely fashion.

The following diagram shows how the RPI works. the business logic in the client invokes a Proxy Interface implemented by a PRI proxy adapter class. The RPI proxy makes a request to the service.

The request is handled by an RPI server adapter class, which invokes the service’s business logic via an interface. It then sends back a reply to the RPI proxy, which returns the result to the client’s business logic.

The proxy interface usually encapsulates the underlying communication protocol. There are numerous protocols to choose from. we focus on the most popular protocols REST and gRPC.

REST API

A key concept in REST is a resource, which typically represents a single business object, such as a Customer or Product, or a collection of business objects. REST uses the HTTP verbs for manipulating resources, which are referenced using a URL. For example, a GET request returns the representation of a resource, which is often in the form of an XML document or JSON object, although other formats such as binary can be used. A POST request creates a new resource, and a PUT request updates a resource.

Challenge with REST API

  1. FETCHING MULTIPLE RESOURCES IN A SINGLE REQUEST REST resources often focus on business objects like Customers and Orders, posing a challenge for retrieving multiple related objects in one request. For instance, obtaining an Order and its associated Customer would typically require multiple API calls. A common workaround is to enhance the API so clients can fetch related resources in a single call, such as using a GET request with an “expand” query parameter to specify related resources. While effective in many cases, this approach can be complex and time-consuming to implement, which has contributed to the rise of alternative technologies like GraphQL for more streamlined data retrieval.
  2. MAPPING OPERATIONS TO HTTP VERBS A notable REST API design challenge is how to assign specific operations on a business object to the correct HTTP verb. For instance, updating an order might involve various actions like cancelling or revising it, and not all updates are necessarily idempotent, which is required for using the HTTP PUT method. A common approach is to create sub-resources for distinct update actions, such as using POST for cancelling (POST /orders/{orderId}/cancel) or revising an order (POST /orders/{orderId}/revise). Another method is to include the action as a URL query parameter. However, these methods may not fully adhere to REST principles. This difficulty in mapping operations to HTTP verbs has contributed to the popularity of alternative technologies like gRPC.

Benefits And Drawbacks Of REST

There are numerous benefits to using REST:

  • It’s simple and familiar.
  • You can test an HTTP API from within a browser using for example, the Postman plugin, or from the command line using curl (assuming JSON or some other text format is used).
  • It directly supports request/response style communication.
  • HTTP is of course, firewall-friendly.
  • It doesn’t require an intermediate broker, which simplifies the system’s architecture.

There are some drawbacks to using REST:

  • It only supports the request/response style of communication.
  • Reduced availability. Because the client and service communicate directly without an intermediary to buffer messages, they must both be running for the duration of the exchange.
  • Clients must know the locations (URLs) of the service instances(s). This is a nontrivial problem in a modern application. Clients must use what is known as a service discovery mechanism to locate service instances.
  • Fetching multiple resources in a single request is challenging.
  • It’s sometimes difficult to map multiple update operations to HTTP verbs.

Using gRPC

REST APIs often struggle with limited HTTP verbs to handle multiple update operations effectively. gRPC provides an alternative by using a binary message-based protocol which emphasizes an API-first approach. It utilizes Protocol Buffers (Protobuf), a language-neutral serialization system developed by Google, allowing developers to define APIs in a Protocol Buffers-based Interface Definition Language (IDL). This setup enables automatic generation of client and server code across various programming languages like Java, C#, NodeJS, and GoLang, using the Protocol Buffer compiler. gRPC APIs run over HTTP/2, supporting both simple request/response and streaming RPCs, where servers can send a stream of messages to clients or vice versa. This technology supports the creation of well-defined service interfaces with strongly typed methods, offering a robust framework for handling diverse and complex communication patterns in microservices architecture.

Benefits And Drawbacks Of gRPC

gRPC has several benefits:

  • It’s straightforward to design an API that has a rich set of update operations.
  • It has an efficient, compact IPC mechanism, especially when exchanging large messages.
  • Bidirectional streaming enables both RPI and messaging styles of communication.
  • It enables interoperability between clients and services written in a wide range of languages.

gRPC also has several drawbacks:

  • It takes more work for JavaScript clients to consume gRPC-based API than REST/JSON-based APIs.
  • Older firewalls might not support HTTP/2.

gRPC is a compelling alternative to REST, but like REST, it’s a synchronous communication mechanism, so it also suffers from the problem of partial failure.

Communicating Using The Asynchronous Messaging Pattern

When using messaging, services communicate by asynchronously exchanging messages. A messaging-based application typically uses a message broker, which acts as an intermediary between the services. A service client makes a request to a service by sending it a message. If the service instance is expected to reply, it will do so by sending a separate message back to the client. Because the communication is asynchronous, the client doesn’t block waiting for a reply. Instead, the client is written assuming that the reply won’t be received immediately.

Overview Of Messaging

From the book Enterprise Integration Patterns by Gregor Hohpe and Bobby Woolf

Messages are exchanged over message channels. A sender (an application or service) writes a message to a channel, and a receiver (an application or service) reads messages from a channel. Let’s look at messages and then look at channels.

About Messages

A message consists of a header and a message body.

The header is a collection of name-value pairs, and metadata that describes the data being sent. In addition to name-value pairs provided by the message’s sender, the message header contains name-value pairs, such as a unique message id generated by either the sender or the messaging infrastructure, and an optional return address, which specifies the message channel that a reply should be written to. The message body is the data being sent, in either text or binary format.

There are several different kinds of messages:

  • Document — A generic message that contains only data. The receiver decides how to interpret it. The reply to a command is an example of a document message.
  • Command — A message that’s the equivalent of an RPC request. It specifies the operation to invoke and its parameters.
  • Event — A message indicating that something notable has occurred in the sender. An event is often a domain event, which represents a state change of a domain object such as an Order, or a Customer.

We will mostly focus on Commands & Events in this blog post

About Message Channels

messages are exchanged over channels The business logic in the sender invokes a sending port interface, which encapsulates the underlying communication mechanism. The sending port is implemented by a message sender adapter class, which sends a message to a receiver via a message channel. A message channel is an abstraction of the messaging infrastructure. A message handler adapter class in the receiver is invoked to handle the message. It invokes a receiving port interface implemented by the consumer’s business logic. Any number of senders can send messages to a channel. Similarly, any number of receivers can receive messages from a channel.

Message Channels Infrastructure

It is very important to understand the two kinds of channels: Point-To-Point and Publish-Subscribe

  • Point-to-Point channel delivers a message to exactly one of the consumers that is reading from the channel. Services use point-to-point channels for the one-to-one interaction styles described earlier. For example, a command message is often sent over a point-to-point channel.
  • Publish-Subscribe channel delivers each message to all of the attached consumers. Services use publish-subscribe channels for the one-to-many interaction styles described earlier. For example, an event message is usually sent over a publish-subscribe channel.

Now that we have a clear understanding of asynchronous communication, including the concepts of messages and message channels, it is appropriate to proceed with exploring the implementation of various communication mechanisms provided by asynchronous communication frameworks.

Implementing Request/Response And Asynchronous Request/Response

When a client and service interact using either request/response or asynchronous request/response, the client sends a request and the service sends back a reply. The difference between the two interaction styles is that with request/response the client expects the service to respond immediately, whereas with asynchronous request/ response there is no such expectation. Messaging is inherently asynchronous, so only provides asynchronous request/response. But a client could block until a reply is received.

The client and service implement the asynchronous request/response style interaction by exchanging a pair of messages. As the diagram shows, the client sends a command message, which specifies the operation to perform, and its parameters, to a point-to-point messaging channel owned by a service. The service processes the requests and sends a reply message, which contains the outcome, to a point-to-point channel owned by the client.

Asynchronous Request/Response

From the above diagram, the client must tell the service where to send a reply message and must match reply messages to requests. Fortunately, solving these two problems isn’t that difficult. The client sends a command message that has a reply channel header. The server writes the reply message, which contains a correlation id that has the same value as the message identifier, to the reply channel. The client uses the correlation id to match the reply message with the request.

Because the client and service communicate using messaging, the interaction is inherently asynchronous. In theory, a messaging client could block until it receives a reply, but in practice, the client will process replies asynchronously. What’s more, replies are typically processed by any one of the client’s instances.

Implementing One-Way Notifications

Implementing one-way notifications is straightforward using asynchronous messaging. The client sends a message, typically a command message, to a point-to-point channel owned by the service. The service subscribes to the channel and processes the message. It doesn’t send back a reply. You can think of this as the same diagram we had at “Asynchronous Request/Response” but without the reply channel.

Implementing Publish/Subscribe

A client publishes a message to a publish-subscribe channel that is read by multiple consumers. Services use publish/subscribe to publish domain events, which represent changes to domain objects. The service that publishes the domain events owns a publish-subscribe channel, whose name is derived from the domain class. A service that’s interested in a particular domain object’s events only has to subscribe to the appropriate channel.

Publish/Subscribe

Implementing Publish/Async Response

The publish/async responses interaction style is a higher-level style of interaction that’s implemented by combining elements of publish/subscribe and request/response. A client publishes a message that specifies a reply channel header to a publish-subscribe channel. A consumer writes a reply message containing a correlation id to the reply channel. The client gathers the responses by using the correlation id to match the reply messages with the request.

Each service in your application that has an asynchronous API will use one or more of these implementation techniques. A service that has an asynchronous API for invoking operations will have a message channel for requests. Similarly, a service that publishes events will publish them to an event message channel.

Using a message broker

A messaging-based application typically uses a message broker, an infrastructure service through which the service communicates. But a broker-based architecture isn’t the only messaging architecture. You can also use a brokerless-based messaging architecture, in which the services communicate with one another directly (we will not cover this topic in this blog post)

Overview Of Broker-Based Messaging

A message broker is an intermediary through which all messages flow. A sender writes the message to the message broker, and the message broker delivers it to the receiver. An important benefit of using a message broker is that the sender doesn’t need to know the network location of the consumer. Another benefit is that a message broker buffers messages until the consumer is able to process them.

There are many message brokers to choose from. Examples of popular open-source message brokers include the following:

  • ActiveMQ
  • RabbitMQ
  • Apache Kafka

Each broker makes different trade-offs. For example, a very low-latency broker might not preserve ordering, make no guarantees to deliver messages, and only store messages in memory. A messaging broker that guarantees delivery and reliably stores messages on disk will probably have higher latency.

Which kind of message broker is the best fit depends on your application’s requirements. It’s even possible that different parts of your application will have different messaging requirements.

Implementing Message Channels Using A Message Broker

Each message broker implements the message channel concept in a different way. As the table shows, JMS message brokers such as ActiveMQ have queues and topics. AMQP-based message brokers such as RabbitMQ have exchanges and queues. Apache Kafka has topics, AWS Kinesis has streams, and AWS SQS has queues. What’s more, some message brokers offer more flexible messaging than the message and channel abstraction described in this chapter.

Message Brokers List

Almost all the message brokers described here support both point-to-point and publish-subscribe channels. The one exception is AWS SQS, which only supports point-to-point channels.

Problems With Message Brokers

Competing Receivers And Message Ordering

One challenge is how to scale out message receivers while preserving message ordering. It’s a common requirement to have multiple instances of a service in order to process messages concurrently. Moreover, even a single service instance will probably use threads to concurrently process multiple messages. Using multiple threads and service instances to concurrently process messages increases the throughput of the application. But the challenge with processing messages concurrently is ensuring that each message is processed once and in order.

For example, imagine that there are three instances of a service reading from the same point-to-point channel and that a sender publishes Order Created, Order Updated, and Order Cancelled event messages sequentially. A simplistic messaging implementation could concurrently deliver each message to a different receiver. Because of delays due to network issues or garbage collections, messages might be processed out of order, which would result in strange behavior. In theory, a service instance might process the Order Cancelled message before another service processes the Order Created message!

A common solution, used by modern message brokers like Apache Kafka and AWS Kinesis, is to use sharded (partitioned) channels. The following diagram shows how this works. There are three parts to the solution:

  1. A sharded channel consists of two or more shards, each of which behaves like a channel.
  2. The sender specifies a shard key in the message’s header, which is typically an arbitrary string or sequence of bytes. The message broker uses a shard key to assign the message to a particular shard/partition. It might, for example, select the shard by computing the hash of the shard key modulo the number of shards.
  3. The messaging broker groups together multiple instances of a receiver and treats them as the same logical receiver. Apache Kafka, for example, uses the term consumer group. The message broker assigns each shard to a single receiver. It reassigns shards when receivers start up and shut down.
Sharded Channels Architecture

In this example, each Order event message has the orderId as its shard key. Each event for a particular order is published to the same shard, which is read by a single consumer instance. As a result, these messages are guaranteed to be processed in order.

Handling Duplicate Messages

Another challenge you must tackle when using messaging is dealing with duplicate messages. A message broker should ideally deliver each message only once, but guaranteeing exactly-once messaging is usually too costly. Instead, most message brokers promise to deliver a message at-least-once.

When the system is working normally, a message broker that guarantees at-least-once delivery will deliver each message only once. However, a failure of a client, network, or message broker can result in a message being delivered multiple times. Say a client crashes after processing a message and updating its database — but before acknowledging the message. The message broker will deliver the unacknowledged message again, either to that client when it restarts or to another replica of the client.

Ideally, you should use a message broker that preserves ordering when redelivering messages.

Imagine that the client processes an Order Created event followed by an Order Cancelled event for the same Order, and that somehow the Order Created event wasn’t acknowledged. The message broker should redeliver both the Order Created and Order Cancelled events. If it only redelivers the Order Created, the client may undo the cancelling of the Order.

There are a couple of different ways to handle duplicate messages:

  • Write idempotent message handlers.
  • Track messages and discard duplicates.

Let’s look at each option at a high-level and we will have a separate blog post for each of them later on.

Write Idempotent Message Handlers

If the application logic that processes messages is idempotent, then duplicate messages are harmless. Application logic is idempotent if calling it multiple times with the same input values has no additional effect. For instance, cancelling an already-cancelled order is an idempotent operation. So is creating an order with a client-supplied ID.

An idempotent message handler can be safely executed multiple times, provided that the message broker preserves ordering when redelivering messages.

Unfortunately, application logic is often not idempotent. Or you may be using a message broker that doesn’t preserve ordering when redelivering messages. Duplicate or out-of-order messages can cause bugs. In this situation, you must write message handlers that track messages and discard duplicate messages.

I will discuss Idempotency in details in a later post

Tracking Messages And Discarding Duplications

Consider, for example, a message handler that authorizes a consumer credit card. It must authorize the card exactly once for each order. This example of application logic has a different effect each time it’s invoked. If duplicate messages caused the message handler to execute this logic multiple times, the application would behave incorrectly. The message handler that executes this kind of application logic must become independent by detecting and discarding duplicate messages. A simple solution is for a message consumer to track the messages that it has processed using the message id and discard any duplicates. It could, for example, store the message id of each message that it consumed in a database table. The following diagram shows how to do this using a dedicated table.

Tracking Messages Flow

When a consumer handles a message, it records the message id in the database table as part of the transaction that creates and updates business entities. In this example, the consumer inserts a row containing the message id into a PROCESSED_MESSAGES table. If a message is a duplicate, the INSERT will fail and the consumer can discard the message.

Another option is for a message handler to record message ids in an application table instead of a dedicated table. This approach is particularly useful when using a NoSQL database that has a limited transaction model, so it doesn’t support updating two tables as part of a database transaction.

Conclusion

In summary, the choice of communication style in a microservices architecture is crucial for the overall efficiency and scalability of an application. Throughout this post, we’ve explored various communication mechanisms — from synchronous calls to asynchronous messaging — each with its unique advantages and suitable use cases. As we have seen, the right communication strategy not only enhances performance but also ensures resilience and flexibility in your service interactions.

When choosing a communication style, it’s essential to consider factors such as the nature of the service interactions, the need for real-time data, and the complexity of the services involved. Remember, the goal is to foster a robust architecture that can evolve with your organizational needs and technological advancements.

References

Microservices
System Design Interview
Software Engineering
Software Development
Solution Architecture
Recommended from ReadMedium