Synchronous communication between microservices is like a direct phone call: the caller waits for the callee to respond before continuing. This might seem straightforward, but the implications for system behavior, especially under load, are profound.

Let’s see this in action. Imagine a simple OrderService that needs to check InventoryService before confirming an order.

# OrderService (Python/Flask example)
from flask import Flask, request, jsonify
import requests # For REST
import grpc # For gRPC

app = Flask(__name__)

# --- REST Example ---
@app.route('/create_order_rest', methods=['POST'])
def create_order_rest():
    order_data = request.json
    item_id = order_data.get('item_id')
    quantity = order_data.get('quantity')

    try:
        # Synchronous REST call to InventoryService
        inventory_response = requests.get(f"http://inventory-service:5001/inventory/{item_id}")
        inventory_response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
        inventory_data = inventory_response.json()

        if inventory_data.get('available') >= quantity:
            # Proceed with order creation...
            return jsonify({"message": "Order created successfully (REST)"}), 201
        else:
            return jsonify({"message": "Insufficient inventory (REST)"}), 400
    except requests.exceptions.RequestException as e:
        print(f"Error communicating with InventoryService (REST): {e}")
        return jsonify({"message": "Service temporarily unavailable (REST)"}), 503

# --- gRPC Example ---
# Assuming you have a generated gRPC client for InventoryService
# from inventory_pb2_grpc import InventoryServiceStub
# from inventory_pb2 import GetInventoryRequest

# def get_inventory_grpc_client():
#     channel = grpc.insecure_channel('inventory-service:50051')
#     return InventoryServiceStub(channel)

# @app.route('/create_order_grpc', methods=['POST'])
# def create_order_grpc():
#     order_data = request.json
#     item_id = order_data.get('item_id')
#     quantity = order_data.get('quantity')

#     inventory_client = get_inventory_grpc_client()
#     request = GetInventoryRequest(item_id=item_id)

#     try:
#         # Synchronous gRPC call to InventoryService
#         inventory_response = inventory_client.GetInventory(request)

#         if inventory_response.available >= quantity:
#             # Proceed with order creation...
#             return jsonify({"message": "Order created successfully (gRPC)"}), 201
#         else:
#             return jsonify({"message": "Insufficient inventory (gRPC)"}), 400
#     except grpc.RpcError as e:
#         print(f"Error communicating with InventoryService (gRPC): {e.code()} - {e.details()}")
#         return jsonify({"message": "Service temporarily unavailable (gRPC)"}), 503

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

In this example, OrderService makes a direct, blocking call to InventoryService. The OrderService thread or process is held up until InventoryService responds. This pattern is simple to implement because it mirrors traditional client-server interactions.

The core problem synchronous communication solves is the need for immediate data or confirmation from another service to proceed. If an order cannot be placed without knowing current inventory, a synchronous call is the most direct way to get that information.

Internally, when OrderService makes a REST call, it’s forming an HTTP request, sending it over the network, and then waiting for an HTTP response. For gRPC, it’s serializing a request object, sending it over a TCP connection (typically), and waiting for a serialized response object. The underlying mechanism is network I/O, which is inherently blocking unless handled asynchronously.

The exact levers you control are:

  • Timeouts: How long the calling service will wait for a response before giving up. This is crucial to prevent cascading failures. For requests in Python, you’d use requests.get(url, timeout=5). For gRPC, it’s often configured at the channel level or per-call.
  • Retries: Whether and how many times to re-attempt a failed synchronous call. This can mask transient network issues but also exacerbate load on a struggling service.
  • Circuit Breakers: A pattern to automatically stop making calls to a service that is consistently failing, preventing the caller from wasting resources and allowing the failing service time to recover.
  • Service Discovery: How the calling service finds the network address of the called service. This is usually handled by external systems (like Kubernetes DNS, Consul, etc.) and is independent of the communication protocol itself.

What most people don’t realize is that the latency of synchronous calls is additive. If OrderService calls InventoryService (50ms latency) and then PaymentService (50ms latency), the total latency for the OrderService operation is at least 100ms, plus the time spent processing. If InventoryService is slow, OrderService is slow. If PaymentService is slow, OrderService is slow. This chain reaction is the primary driver for adopting asynchronous patterns when possible.

The next logical step is understanding how to mitigate the blocking nature of synchronous calls using client-side patterns like retries and circuit breakers.

Want structured learning?

Take the full Microservices course →