In the previous article, we explored how large applications gradually evolve from monoliths into collections of smaller, independently deployable services. Instead of one massive application responsible for authentication, payments, inventory, notifications, and analytics, we now have multiple services, each responsible for a single business capability.
At first glance, this seems like a clean and elegant solution. Every service owns its own logic, its own database, and can be deployed independently without affecting the rest of the system.
But splitting an application into multiple services immediately creates a new challenge.
The components that once lived inside the same process now live on different machines.
Inside a monolithic application, communication is almost effortless. If the payment module needs information about a user, it simply calls a function from the user module. The call happens inside the same process, uses the same memory space, and usually completes in a fraction of a millisecond. Developers rarely stop to think about this communication because, from their perspective, it is almost free.
Microservices change this assumption entirely.
The payment service no longer has direct access to the user service. They may be running on different servers, in different containers, or even in different regions of the world. Every interaction between them must now travel through a network.
What was once a simple function call has become a network request.
This seemingly small architectural change has enormous consequences.
Network requests introduce latency. They can fail unexpectedly. They may experience congestion, timeouts, packet loss, or temporary service outages. Unlike local function calls, network communication is inherently unreliable.
This is why experienced engineers often say that moving to microservices means moving into the world of distributed systems.
The application is no longer just executing code.
It is coordinating communication between independent systems.
Why Communication Becomes a System Design Problem
Imagine a customer placing an order on an e-commerce platform.
From the customer's perspective, the process appears straightforward. They click the "Place Order" button, wait a few seconds, and receive a confirmation message.
Behind the scenes, however, the request may travel through several independent services before the order is successfully processed.
The order service must verify the customer.
The inventory service must confirm that the requested items are available.
The payment service must process the transaction.
The notification service must send a confirmation email or SMS.
Each of these services performs its own responsibility, but together they must behave as if they were a single application.
Notice something interesting about this interaction.
The Order Service is not solving business problems alone.
It spends a significant amount of time communicating with other services.
As systems grow larger, communication itself becomes one of the biggest consumers of time and resources.
This is one of the reasons distributed systems are fundamentally different from traditional applications.
The challenge is no longer writing business logic.
The challenge is coordinating independent systems efficiently and reliably.
Not Every Conversation Between Services Is the Same
Suppose the Order Service needs to verify whether a product exists before allowing a purchase.
The answer is needed immediately.
The customer is waiting.
The Order Service cannot continue until it receives a response.
Now consider a different situation.
A payment has been completed successfully, and the customer should receive a confirmation email.
Does the customer really need to wait until the email has been delivered?
Probably not.
The order should complete successfully even if the email arrives a few seconds later.
Now imagine another scenario.
A recommendation engine wants to learn which products users purchase so that it can improve future recommendations.
Should the payment process slow down while waiting for the recommendation engine to process analytics?
Again, the answer is no.
These examples illustrate an important principle in distributed systems:
Not every interaction between services has the same urgency.
Some requests require an immediate response.
Others can happen later.
Some require a direct conversation.
Others simply need to notify another service that an event has occurred.
Understanding these differences is the key to choosing the right communication mechanism.
Synchronous vs Asynchronous Communication
Before discussing REST, gRPC, or messaging systems, we need to understand two fundamental communication models that appear throughout distributed systems.
The first is synchronous communication.
In synchronous communication, one service sends a request and waits for the response before continuing.
It is very similar to a phone call.
When you call someone, the conversation happens in real time. You ask a question and wait for the answer before continuing the discussion.
Most web applications work this way.
When your browser requests a webpage, it waits until the server responds.
Likewise, when one microservice calls another using REST or gRPC, it often waits for the response before moving forward.
This approach is simple to understand because it closely resembles traditional programming.
However, it also creates dependencies.
If Service B becomes slow, Service A also becomes slow.
If Service B becomes unavailable, Service A may fail entirely.
In other words, synchronous communication couples the availability and performance of multiple services.
Asynchronous communication takes a very different approach.
Instead of waiting for an immediate response, one service simply publishes a message and continues its own work.
Receiving services process that message whenever they are ready.
A useful analogy is email.
When you send an email, you do not wait with your computer open until the recipient replies.
You send the message and continue with your day.
The recipient responds later.
Distributed systems often behave in exactly the same way.
This model dramatically reduces coupling between services.
The Order Service no longer needs to know whether the Notification Service is currently running.
Its responsibility ends once the message has been successfully placed into the queue.
The notification can be processed seconds—or even minutes—later without affecting the customer experience.
As systems become larger, this asynchronous style of communication becomes increasingly common because it improves resilience and allows services to operate independently.
Three Ways Services Usually Communicate
Almost every distributed system relies on one of three primary communication mechanisms.
The first is REST, which has become the standard way of building web APIs over the last two decades. It is simple, human-readable, and supported by virtually every programming language and framework.
The second is gRPC, a high-performance communication framework designed for fast and efficient communication between services. Instead of exchanging human-readable JSON, gRPC uses a compact binary protocol, making it significantly faster in many internal service-to-service scenarios.
The third approach uses message queues, where services communicate indirectly by publishing events instead of sending direct requests. Rather than waiting for immediate responses, services exchange information through brokers such as queues or event streams, enabling asynchronous communication and improving system resilience.
Although all three approaches allow services to communicate, they solve different problems.
Choosing between them is rarely about which technology is better.
It is about understanding the nature of the conversation taking place between services.
Should the sender wait for a response?
Does the receiver need to respond immediately?
Can the work happen later?
Should failures affect the user?
The answers to these questions determine the most appropriate communication model.
And this is where our discussion truly begins.
REST: The Language of the Web
Long before microservices became popular, applications already needed a way to communicate over networks.
Web browsers needed to request web pages.
Mobile applications needed to fetch user profiles.
Third-party developers needed access to payment systems, maps, weather data, and social media platforms.
The internet itself needed a common language that every application, regardless of the programming language it was written in, could understand.
This is where REST entered the picture.
REST, which stands for Representational State Transfer, is not a protocol or a programming language. It is an architectural style proposed by Roy Fielding in his doctoral dissertation in the year 2000. Rather than introducing a completely new communication protocol, REST embraced technologies that were already powering the web—primarily HTTP.
This decision played a significant role in REST's widespread adoption.
Instead of asking developers to learn an entirely new ecosystem, REST leveraged concepts they were already familiar with. URLs identified resources, HTTP methods described operations, and responses were returned using formats such as JSON or XML.
Over time, REST became the de facto standard for building APIs, and today, it powers a significant portion of the modern internet.
Whether you're checking your bank balance, booking a flight, ordering food, or scrolling through social media, there's a good chance your device is communicating with backend services through REST APIs.
Thinking in Resources Instead of Functions
One of the biggest mindset shifts when learning REST is understanding that it is resource-oriented, not function-oriented.
In traditional programming, we tend to think in terms of actions.
We write functions like:
createUser()
getUser()
deleteUser()
updateUser()
REST encourages us to think differently.
Instead of focusing on actions, we focus on resources.
A user is a resource.
An order is a resource.
A product is a resource.
A payment is a resource.
Once a resource exists, HTTP methods describe what we want to do with it.
For example:
GET /users/125
POST /users
PUT /users/125
DELETE /users/125
Notice how the URL represents what we are working with, while the HTTP method represents what operation we want to perform.
This separation makes APIs easier to understand because they resemble the structure of the real-world entities they represent.
How a REST Request Travels Through the System
Let's consider a simple example.
A customer opens an e-commerce application and wants to view the details of a product.
The frontend application sends an HTTP request to the Product Service.
The Product Service processes the request, retrieves the necessary information from its database, converts the result into JSON, and sends it back to the client.
The interaction looks something like this:
From the user's perspective, this entire interaction feels almost instantaneous.
Behind the scenes, however, several things have happened.
The request travelled through the internet.
The server authenticated the client.
Business logic was executed.
A database query was performed.
The response was serialised into JSON.
Finally, the data was transmitted back over the network.
Every REST request follows this general request-response lifecycle.
Why REST Became So Popular
One of REST's greatest strengths is its simplicity.
Almost every programming language today can send an HTTP request.
Browsers understand HTTP natively.
Firewalls are designed to work with HTTP traffic.
Cloud platforms, API gateways, reverse proxies, load balancers, and CDNs all understand HTTP exceptionally well.
Because of this universal support, REST APIs can be consumed by virtually anything.
A web browser.
A mobile application.
A desktop application.
Another backend service.
Even command-line tools like curl.
This universality is one of the reasons REST became the default communication mechanism for modern applications.
It reduced friction.
Developers no longer needed specialised libraries or proprietary communication protocols.
If two systems could speak HTTP, they could usually communicate with one another.
REST Is Stateless - And That Is a Good Thing
One of the defining characteristics of REST is that it is stateless.
At first, the word sounds technical, but the idea is remarkably simple.
Every request should contain all the information required to process it.
The server should not rely on memory from previous requests.
Imagine asking someone for directions.
If every time you ask, you provide your current location and destination, they can answer immediately.
They don't need to remember your previous conversations.
REST works in the same way.
Suppose a client sends the following request:
GET /orders/1254
Authorization: Bearer <token>
The request already contains everything the server needs.
It specifies:
- Which resource is being requested.
- Who is making the request.
- Authentication credentials.
- Any additional headers or parameters.
The server processes the request, sends the response, and then forgets everything about that interaction.
The next request starts from scratch.
This stateless design provides significant scalability benefits.
Since servers do not need to remember client sessions, incoming requests can be distributed across multiple application instances using a load balancer.
Any server can process any request because every request is self-contained.
This aligns perfectly with the horizontal scaling principles we discussed earlier in this series.
JSON: The Universal Language of REST
Although REST itself does not require JSON, the two have become almost inseparable.
JSON is lightweight, human-readable, and supported by virtually every modern programming language.
A typical REST response might look like this:
{
"id": 105,
"name": "Mechanical Keyboard",
"price": 89.99,
"stock": 42
}
One reason developers appreciate JSON is that it is easy to inspect.
If something goes wrong, developers can open browser developer tools, examine the response, and immediately understand what the server returned.
This readability makes debugging significantly easier than many binary communication formats.
However, this convenience comes with a trade-off.
Text-based data is generally larger than binary data.
A JSON document contains field names, punctuation, quotation marks, and whitespace, all of which increase the size of the response.
For applications serving millions of requests every minute, these additional bytes become significant.
This limitation eventually motivated the development of faster communication mechanisms such as gRPC, which we'll explore in the next part.
Where REST Truly Shines
REST is exceptionally well suited for communication between clients and servers.
When a web browser requests product information, when a mobile application fetches a user's profile, or when an external partner integrates with your platform, REST is often an excellent choice.
Its simplicity, interoperability, and widespread tooling make it ideal for public-facing APIs.
It is also highly cache-friendly.
Since REST is built on HTTP, it naturally benefits from HTTP caching, proxy servers, reverse proxies, and Content Delivery Networks (CDNs). Responses that do not change frequently can often be cached closer to users, reducing latency and improving performance.
This is one of the reasons many public APIs continue to rely on REST despite the emergence of newer communication technologies.
Its ecosystem is mature, battle-tested, and universally understood.
REST Is Not Perfect
Despite its popularity, REST is not the answer to every communication problem.
Because REST relies on HTTP and typically exchanges JSON, each request carries a certain amount of overhead.
Headers must be transmitted.
JSON must be serialised by the sender.
JSON must be parsed by the receiver.
Every interaction involves opening, processing, and completing an HTTP request-response cycle.
For occasional communication, this overhead is almost negligible.
But imagine hundreds of microservices communicating thousands of times every second.
The cumulative cost becomes noticeable.
Applications requiring extremely low latency or high throughput often begin looking for more efficient alternatives.
Another limitation is that REST is fundamentally request-driven.
One service asks another for information and waits until the response arrives.
As we discussed in the previous part, this synchronous style of communication creates dependencies between services.
If one service becomes slow, the calling service also slows down.
If one service becomes unavailable, requests may begin failing throughout the system.
These limitations do not make REST a poor choice.
They simply highlight that every communication model involves trade-offs.
And understanding those trade-offs is precisely what system design is about.
gRPC: Built for High-Performance Service-to-Service Communication
REST revolutionised the way applications communicate over the web, and even today it remains one of the most widely adopted architectural styles for building APIs. It is simple, readable, and supported by virtually every programming language and framework.
However, as organisations embraced microservices, engineers began noticing a different kind of challenge.
The majority of service-to-service communication was no longer happening between browsers and backend servers.
Instead, it was happening between backend services themselves.
A single user request could trigger communication between dozens of internal services.
An Order Service might call the Inventory Service, which in turn communicates with the Pricing Service, the Recommendation Service, the Shipping Service, and the Notification Service before the original request is completed.
Each REST call may take only a few milliseconds, but when hundreds of these calls occur during the processing of a single request—and millions more occur every day—the overhead begins to accumulate.
The issue wasn't that REST was slow.
The issue was that REST was designed primarily for interoperability and simplicity rather than maximum performance.
This is where gRPC enters the picture.
Developed by Google, gRPC is a high-performance Remote Procedure Call (RPC) framework specifically designed for efficient communication between services.
Unlike REST, which encourages thinking in terms of resources and HTTP operations, gRPC focuses on calling methods on remote services almost as if they were local functions.
This makes service-to-service communication feel much closer to traditional programming while retaining the advantages of distributed systems.
Understanding Remote Procedure Calls
Before understanding gRPC, it helps to understand what a Remote Procedure Call (RPC) actually means.
Imagine writing the following function in your application:
calculateShipping(orderId)
Inside a monolithic application, calling this function is straightforward.
The code executes within the same process, accesses local memory, and immediately returns a result.
Now imagine that the shipping logic has been extracted into its own microservice.
The function still appears to exist, but in reality it now executes on an entirely different machine.
Instead of calling local code, the application must:
- Serialise the request.
- Send it across the network.
- Wait for the remote server to execute the operation.
- Receive the response.
- Deserialise the result.
From the developer's perspective, however, the interaction still resembles a normal function call.
This is the philosophy behind RPC systems.
Rather than thinking in terms of URLs and HTTP resources, developers think in terms of methods and services.
How gRPC Communicates
One of the biggest differences between REST and gRPC lies in how data is transmitted.
REST typically exchanges data using JSON.
JSON is human-readable, easy to debug, and universally supported.
However, it is also relatively verbose.
Consider a simple JSON response:
{
"productId": 101,
"name": "Mechanical Keyboard",
"price": 89.99
}
Every field name is transmitted over the network.
Quotation marks, commas, braces, and whitespace all contribute to the size of the payload.
For applications serving millions of requests every second, these extra bytes matter.
gRPC takes a different approach.
Instead of JSON, it uses Protocol Buffers (Protobuf), a compact binary serialisation format.
Rather than transmitting descriptive text, Protobuf represents data in a highly efficient binary format.
The resulting payloads are significantly smaller.
Smaller payloads require less bandwidth.
Less bandwidth means faster transmission.
Faster transmission contributes to lower latency and higher throughput.
Although these improvements may appear minor for a single request, they become substantial when multiplied across billions of service calls every day.
Defining APIs Before Writing Code
One particularly elegant aspect of gRPC is that communication contracts are defined before implementation begins.
Instead of writing API endpoints directly, developers first create a Protocol Buffer definition file, commonly called a .proto file.
This file describes:
- The services available.
- The methods each service exposes.
- The structure of every request.
- The structure of every response.
Conceptually, it looks like this:
service PaymentService {
rpc ProcessPayment(PaymentRequest)
returns (PaymentResponse);
}
From this single definition, tools automatically generate client libraries and server code in multiple programming languages.
This process eliminates much of the repetitive work developers traditionally perform when building APIs.
More importantly, both the client and the server now share the same contract.
This significantly reduces integration errors because both sides are generated from the same specification.
HTTP/2 Makes a Difference
Another reason gRPC performs exceptionally well is that it is built on top of HTTP/2 rather than traditional HTTP/1.1.
Without diving too deeply into networking internals, HTTP/2 introduces several improvements that make communication more efficient.
Instead of opening multiple independent connections, HTTP/2 allows many requests and responses to travel simultaneously over a single connection.
This reduces connection overhead and allows multiple conversations to happen concurrently.
HTTP/2 also compresses headers and improves how data is transmitted across the network.
The result is faster communication with lower latency, especially in environments where services exchange thousands of requests every second.
For large distributed systems, these efficiencies accumulate into significant performance improvements.
Streaming: A Capability REST Doesn't Naturally Provide
Perhaps one of gRPC's most impressive features is its support for streaming.
Traditional REST communication generally follows a simple pattern:
One request.
One response.
The conversation ends.
Sometimes, however, applications need continuous communication rather than isolated requests.
Imagine a live stock market dashboard.
A multiplayer online game.
A GPS navigation system.
A live sports score application.
A monitoring dashboard displaying server metrics in real time.
In these scenarios, repeatedly sending HTTP requests every second becomes inefficient.
gRPC supports multiple communication models.
A client can send one request and receive a continuous stream of responses.
A server can continuously receive streamed requests.
Or both sides can exchange messages simultaneously.
This capability makes gRPC particularly attractive for applications involving live updates and continuous data exchange.
Where gRPC Excels
gRPC is particularly well suited for communication inside distributed systems.
When hundreds of microservices continuously exchange information, performance becomes increasingly important.
The smaller payload sizes, efficient serialisation, persistent HTTP/2 connections, and automatic code generation all contribute to a communication model optimised for speed.
This is why many organisations use gRPC internally while continuing to expose REST APIs to external clients.
External developers appreciate REST because it is simple, human-readable, and easy to integrate.
Internal services benefit from gRPC because efficiency matters more than human readability.
Many modern architectures therefore combine both approaches.
This hybrid architecture allows systems to take advantage of the strengths of each communication model.
gRPC Is Not Always the Right Choice
Despite its impressive capabilities, gRPC is not intended to replace REST entirely.
Because it uses binary Protocol Buffers, developers cannot simply open a browser and inspect responses as easily as they can with JSON.
Debugging often requires specialised tooling.
Public APIs also tend to favour REST because nearly every programming language, browser, and third-party integration platform already understands HTTP and JSON.
REST therefore remains an excellent choice for communication between external clients and backend systems.
gRPC shines when communication happens primarily between trusted internal services where performance, efficiency, and strong API contracts become more important than human readability.
As with every architectural decision we have discussed throughout this series, neither approach is universally better.
REST optimises for simplicity and interoperability.
gRPC optimises for efficiency and performance.
Understanding the requirements of the system is what determines the better choice.
Up to this point, every communication model we have discussed has shared one common characteristic.
Whether we were using REST or gRPC, one service directly contacted another service and waited for it to perform some work.
The communication was immediate.
The sender knew exactly who the receiver was.
The receiver processed the request and returned a response.
While this model works exceptionally well for many scenarios, it also creates an important dependency.
If the receiving service is unavailable, the sender cannot continue.
If the receiving service becomes slow, the sender also becomes slow.
In other words, the health of one service directly affects another.
As distributed systems become larger, these dependencies begin to accumulate.
Imagine a modern e-commerce platform during a festival sale.
A customer clicks the "Place Order" button.
At first glance, the operation seems simple.
The order is created, payment is processed, inventory is updated, a confirmation email is sent, loyalty points are added, analytics are recorded, invoices are generated, warehouse systems are notified, and recommendation engines learn from the purchase.
Although these activities are all triggered by the same user action, they do not all have the same urgency.
The customer certainly expects payment to be processed immediately.
However, the customer does not care whether analytics are updated within five milliseconds or five seconds.
Similarly, recommendation engines can learn about the purchase later without affecting the shopping experience.
This observation leads to one of the most important ideas in distributed systems:
Not every task needs to happen immediately.
Once engineers recognise this, a completely different communication model becomes possible.
Instead of directly asking another service to perform work, a service can simply announce that something has happened.
Any interested service can process that information whenever it is ready.
This is the philosophy behind message queues.
Thinking in Events Instead of Requests
Traditional APIs are request-driven.
One service asks another service to act.
Message queues are event-driven.
Instead of saying,
"Please send an email."
a service simply announces,
"An order has been created."
The service publishing the event does not know who will consume it.
It does not need to know.
Its responsibility ends after successfully publishing the message.
Other services independently decide whether that event is relevant to them.
This subtle difference fundamentally changes the architecture.
Instead of tightly coupling services together, communication becomes loosely coupled.
Imagine dropping a letter into a mailbox.
Your responsibility ends once the letter has been posted.
You do not stand beside the mailbox waiting for the postal worker.
You trust that the postal system will eventually deliver the message.
Message queues operate in much the same way.
Introducing the Message Broker
A message queue introduces an intermediary known as a message broker.
Rather than communicating directly with one another, services communicate through this broker.
The architecture now looks something like this:
Notice what has changed.
The Order Service no longer needs to know anything about the Notification Service.
It does not know whether the Notification Service is running.
It does not know whether analytics are temporarily unavailable.
It simply publishes an event.
The broker takes responsibility for delivering that message to interested consumers.
This greatly reduces dependencies between services.
A Real-World Example
Let's revisit our e-commerce platform.
A customer successfully places an order.
Inside the Order Service, the business transaction completes successfully.
Immediately afterwards, an event named OrderCreated is published to the message broker.
Notice something remarkable.
The Order Service only performs one communication.
The broker handles everything else.
As additional services are introduced in the future—perhaps fraud detection, inventory forecasting, recommendation systems, or customer rewards—the Order Service remains unchanged.
The architecture naturally supports growth.
Why Message Queues Improve Reliability
Suppose the Notification Service suddenly crashes.
What happens?
In a synchronous REST-based system, the Order Service attempts to contact the Notification Service and receives an error.
Depending on how the application is designed, this failure may delay or even fail the entire user request.
With a message queue, the situation is very different.
The Order Service publishes the event successfully.
The broker safely stores the message.
The Notification Service can process it later after recovering.
The customer still receives a successful order confirmation because the critical business operation—the purchase itself—has already completed.
This ability to temporarily decouple producers and consumers is one of the biggest reasons message queues are so widely adopted.
Services no longer have to be available at exactly the same moment.
The broker absorbs temporary failures and smooths communication between systems.
Handling Traffic Spikes
Another major advantage of message queues appears during periods of unusually high traffic.
Imagine an online retailer during Black Friday.
Millions of customers begin placing orders simultaneously.
The Order Service processes purchases as quickly as possible.
However, sending emails, generating invoices, updating analytics, and notifying warehouse systems all require additional processing time.
If every service attempted to perform all this work immediately, the entire platform could become overwhelmed.
Message queues naturally absorb these spikes.
Incoming events accumulate inside the queue.
Consumers process them at a sustainable rate.
This buffering effect prevents downstream services from becoming overloaded.
Instead of rejecting requests, the system gracefully handles temporary bursts in demand.
This is one reason message queues are often compared to waiting lines at supermarkets.
Customers continue arriving.
Some wait briefly.
Cashiers process them one at a time.
The line absorbs fluctuations in arrival rates.
Scaling Consumers Independently
Suppose the Notification Service cannot keep up with incoming messages.
Unlike synchronous communication, we do not necessarily need a faster server.
Instead, we can simply start more consumer instances.
Each consumer retrieves messages independently.
The workload becomes distributed across multiple workers.
As demand increases, additional consumers can be added.
As traffic decreases, unnecessary consumers can be removed.
This makes message queues naturally compatible with horizontal scaling, one of the concepts we explored earlier in this series.
Rather than scaling the producer, we scale the workers responsible for processing queued tasks.
Popular Message Queue Technologies
Over the years, several technologies have become industry standards for asynchronous communication.
RabbitMQ is one of the most widely used traditional message brokers. It provides reliable message delivery, flexible routing, acknowledgements, retries, and mature tooling, making it an excellent choice for many business applications.
Apache Kafka approaches the problem from a slightly different perspective. Rather than acting solely as a message queue, Kafka functions as a distributed event streaming platform capable of handling enormous volumes of events every second. Organisations use Kafka extensively for log aggregation, analytics pipelines, financial systems, IoT platforms, and real-time data processing.
Cloud providers also offer managed messaging services such as Amazon SQS, Google Cloud Pub/Sub, and Azure Service Bus, allowing teams to adopt asynchronous communication without managing broker infrastructure themselves.
Although these technologies differ in implementation, they all pursue the same goal:
Allow independent systems to communicate without requiring them to be available at exactly the same time.
The Cost of Asynchronous Communication
Message queues provide remarkable flexibility, but they also introduce new challenges.
One obvious trade-off is that communication is no longer immediate.
If an email service processes messages five seconds later, that delay is usually acceptable.
If a payment confirmation arrives five seconds later, the customer may become concerned.
Choosing asynchronous communication therefore requires understanding which operations are time-sensitive and which are not.
Another challenge is debugging.
In synchronous systems, following a request is relatively straightforward.
With asynchronous systems, events may pass through brokers, retries, dead-letter queues, and multiple consumers before finally completing.
Understanding the lifecycle of a single business operation becomes considerably more difficult.
This is why mature event-driven systems rely heavily on distributed tracing, centralised logging, and observability tools.
As systems become more asynchronous, visibility becomes just as important as functionality.
Asynchronous Communication Is About Independence
Perhaps the most important lesson about message queues is that they are not simply another communication technology.
They represent a different philosophy.
REST asks another service to perform work immediately.
gRPC performs the same idea more efficiently.
Message queues remove the requirement for immediate cooperation altogether.
Instead of saying,
"Do this now."
they simply say,
"This happened."
That small change dramatically reduces coupling, improves resilience, smooths traffic spikes, and allows systems to evolve independently.
Throughout this article, we have explored three fundamentally different ways in which services communicate inside modern distributed systems.
We began with REST, the communication style that powers much of today's internet. We then looked at gRPC, a high-performance framework designed specifically for efficient service-to-service communication. Finally, we explored message queues, which abandon direct conversations altogether in favor of asynchronous event-driven communication.
At first, these technologies may appear to compete with one another.
After all, they all allow one system to communicate with another.
So a natural question arises:
Which one should we use?
Interestingly, experienced system designers rarely ask that question.
Instead, they ask a different one.
What kind of conversation are these services trying to have?
That subtle shift in thinking often leads to the correct architectural decision.
There Is No Universal Winner
One of the biggest mistakes engineers make when learning system design is searching for the "best" technology.
Should we always use gRPC because it is faster?
Should we replace every REST API with Kafka?
Should every microservice communicate asynchronously?
The answer to all of these questions is no.
Each communication model was designed to solve a different problem.
Using the wrong one is similar to using a screwdriver to hammer a nail.
The tool is not bad.
It is simply solving a different problem.
REST optimises for simplicity.
gRPC optimises for efficiency.
Message queues optimise for independence.
Understanding those goals is far more valuable than memorising implementation details.
When REST Is the Right Choice
REST remains the most appropriate choice whenever communication involves external clients.
Browsers, mobile applications, desktop applications, and third-party developers all understand HTTP exceptionally well.
If your application exposes a public API, REST is often the safest and most practical option.
Imagine a food delivery application.
A customer's mobile app needs to:
- View nearby restaurants.
- Browse menus.
- Place an order.
- Track delivery status.
- View previous orders.
Each of these actions follows a straightforward request-response pattern.
The client asks for information.
The server responds immediately.
REST fits naturally into this style of interaction.
It is also an excellent choice when readability and interoperability are more important than absolute performance.
Because REST commonly uses JSON, developers can inspect requests with a browser, Postman, or curl, making development and debugging remarkably convenient.
When gRPC Becomes a Better Choice
Now consider the communication happening inside the backend.
The Order Service may need to communicate with the Inventory Service dozens of times every second.
The Recommendation Service may continuously exchange information with machine learning systems.
A Search Service may call several ranking engines before returning results.
These services are all controlled by the same organisation.
There is no need for human-readable JSON.
There is no requirement for browser compatibility.
Performance becomes the primary concern.
This is precisely where gRPC shines.
Its binary serialisation, HTTP/2 transport, persistent connections, and strongly defined contracts make it ideal for internal communication between trusted services.
The user never interacts with gRPC directly.
Instead, it quietly enables fast communication behind the scenes.
Many organisations therefore expose REST APIs publicly while using gRPC internally.
This hybrid architecture combines the accessibility of REST with the efficiency of gRPC.
When Message Queues Are the Better Solution
Some operations simply do not require immediate responses.
Suppose a customer successfully purchases a product.
Several additional activities may need to occur afterwards.
A confirmation email must be sent.
Analytics should record the purchase.
Inventory forecasting should update demand predictions.
Warehouse systems should prepare packaging.
Recommendation engines should learn from customer behaviour.
Notice that none of these tasks should delay the checkout experience.
The customer only cares that the order has been placed successfully.
Everything else can happen afterwards.
This makes asynchronous communication a far better choice than synchronous requests.
Instead of contacting every downstream service directly, the Order Service simply publishes an OrderCreated event.
Every interested service processes that event independently.
The checkout experience remains fast.
The architecture becomes more resilient.
New services can subscribe to the event without modifying existing code.
This ability to evolve naturally is one of the greatest strengths of event-driven architectures.
Modern Systems Rarely Choose Just One
One of the most interesting observations about large technology companies is that they rarely commit to a single communication mechanism.
Instead, they combine several approaches, allowing each to solve the problems it handles best.
A typical architecture might resemble the following:
Notice how each communication model has a clearly defined responsibility.
The mobile application communicates with backend services using REST because HTTP and JSON are universally supported.
Internal services communicate with one another using gRPC because efficiency matters more than readability.
Business events are distributed through message queues because many downstream services do not require immediate responses.
Rather than competing, these technologies complement one another.
Choosing the Right Communication Style
Whenever designing communication between services, it is useful to begin by asking a series of simple questions.
The first question is whether the caller requires an immediate response.
If the answer is yes, synchronous communication such as REST or gRPC is usually appropriate.
The second question concerns performance.
If communication happens frequently between internal services and latency is critical, gRPC often provides significant advantages.
The third question is whether the work must happen immediately.
If the answer is no, asynchronous messaging usually results in a simpler and more resilient architecture.
Finally, consider ownership.
If the API will be consumed by external customers, partners, or public developers, REST remains the most approachable option because of its universal adoption and extensive tooling.
Thinking about the nature of the conversation rather than the technology itself almost always leads to better architectural decisions.
Communication Is About Trade-offs
Throughout this System Design series, one theme has appeared repeatedly.
There is rarely a perfect solution.
Monoliths trade flexibility for simplicity.
Distributed systems trade simplicity for scalability.
Horizontal scaling trades hardware upgrades for coordination.
Microservices trade organisational independence for operational complexity.
Communication mechanisms follow the same pattern.
REST is easy to understand but introduces additional overhead.
gRPC improves efficiency but sacrifices some simplicity and requires specialised tooling.
Message queues increase resilience and decoupling but introduce eventual consistency and make systems harder to observe and debug.
Good system design is therefore not about selecting the newest technology.
It is about selecting the technology whose trade-offs best match the problem you are trying to solve.
Final Thoughts
As applications evolve from monoliths into distributed systems, communication becomes one of the most important architectural concerns.
The quality of a distributed system is determined not only by how well individual services are implemented but also by how effectively those services cooperate.
REST, gRPC, and message queues each represent a different philosophy of communication.
REST emphasises simplicity and broad compatibility.
gRPC emphasises speed, efficiency, and strongly defined contracts.
Message queues emphasise independence, resilience, and asynchronous processing.
The most successful systems are rarely built around just one of these approaches.
Instead, they combine all three, allowing each communication model to solve the problems for which it was designed.
Perhaps that is the most important lesson from this article.
In system design, the goal is not to find a single technology that solves every problem.
The goal is to understand the strengths and limitations of each tool well enough to know when to use it.
And that ability to choose the right tool for the right problem is what ultimately distinguishes a good software engineer from a great system designer.













Top comments (0)