
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

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
- 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.
- 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.

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.

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.

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.

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:
- A sharded channel consists of two or more shards, each of which behaves like a channel.
- 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.
- 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.

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.

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.





