DEV Community

Cover image for Understanding RPC in Microservices
Daniel Azevedo
Daniel Azevedo

Posted on

Understanding RPC in Microservices

Hi devs,

In a microservices architecture, services often need to communicate with each other to fulfill user requests. One popular approach to enable this inter-service communication is through Remote Procedure Call (RPC). In this post, we'll dive into the basics of RPC, its benefits, and its limitations, with a practical example using User, Order, and Product services.

What is RPC?

Remote Procedure Call (RPC) is a protocol that allows a program to execute a procedure (a function or a method) on a remote server as if it were a local function call. Unlike REST or messaging-based communication, RPC offers a more straightforward way to invoke methods across distributed services, making it suitable for synchronous and low-latency interactions.

In an RPC model, instead of sending an HTTP request with a URL and HTTP method (as you would in REST), you directly call a function or method, often with parameters, and wait for the result.

Why Use RPC?

RPC is advantageous in situations where services need quick, synchronous responses. For example:

  • Low Latency: RPC typically has lower latency compared to HTTP REST calls, making it useful for applications where speed is essential.
  • Simplicity: RPC abstracts the details of communication between services, allowing developers to work with methods/functions instead of constructing HTTP requests.
  • Consistency: RPC works well in environments where a consistent protocol is required for data exchange.

However, RPC can lead to tight coupling between services, so it’s best suited for scenarios where services are closely related and often need to share data or invoke each other's functions directly.

Example: User, Order, and Product Services with RPC

Let’s consider a hypothetical e-commerce application where we have three primary services:

  1. User Service: Manages user accounts, login, and profile information.
  2. Order Service: Manages orders placed by users.
  3. Product Service: Manages product listings and availability.

In this example, when a user wants to view their order history, the Order Service might need to get information from the User Service (for user verification) and the Product Service (for product details). Using RPC, each service can make requests to the others directly through remote procedure calls, simplifying data retrieval and reducing overhead.

Implementing RPC with gRPC

To implement RPC in our services, we can use gRPC, a high-performance RPC framework developed by Google. gRPC enables communication between services using HTTP/2 and Protocol Buffers (protobufs), which are more efficient than JSON.

Step 1: Define Protobuf Messages and Services

In gRPC, the interactions between services are defined in a .proto file. Let’s create a .proto file for our example, defining the methods and data structure that will be shared.

syntax = "proto3";

service UserService {
  rpc GetUser(UserRequest) returns (UserResponse);
}

service OrderService {
  rpc GetOrder(OrderRequest) returns (OrderResponse);
}

service ProductService {
  rpc GetProduct(ProductRequest) returns (ProductResponse);
}

message UserRequest {
  int32 userId = 1;
}

message UserResponse {
  int32 userId = 1;
  string userName = 2;
  string userEmail = 3;
}

message OrderRequest {
  int32 orderId = 1;
}

message OrderResponse {
  int32 orderId = 1;
  int32 userId = 2;
  repeated Product products = 3;
}

message ProductRequest {
  int32 productId = 1;
}

message ProductResponse {
  int32 productId = 1;
  string productName = 2;
  float price = 3;
}

message Product {
  int32 productId = 1;
  int32 quantity = 2;
}
Enter fullscreen mode Exit fullscreen mode

This .proto file defines the services and messages for our User, Order, and Product services. Each service has a single method for retrieving details based on IDs.

Step 2: Implement the Services

Here’s how we might implement these services in C#. In this example, we'll use gRPC libraries for C# to define and implement the logic.

User Service
public class UserService : UserService.UserServiceBase
{
    public override Task<UserResponse> GetUser(UserRequest request, ServerCallContext context)
    {
        // Simulating a user record
        var user = new UserResponse
        {
            UserId = request.UserId,
            UserName = "John Doe",
            UserEmail = "johndoe@example.com"
        };
        return Task.FromResult(user);
    }
}
Enter fullscreen mode Exit fullscreen mode
Order Service

The Order Service may need to retrieve products from the Product Service and user data from the User Service.

public class OrderService : OrderService.OrderServiceBase
{
    private readonly ProductServiceClient _productServiceClient;
    private readonly UserServiceClient _userServiceClient;

    public OrderService(ProductServiceClient productServiceClient, UserServiceClient userServiceClient)
    {
        _productServiceClient = productServiceClient;
        _userServiceClient = userServiceClient;
    }

    public override async Task<OrderResponse> GetOrder(OrderRequest request, ServerCallContext context)
    {
        // Simulating an order record
        var order = new OrderResponse
        {
            OrderId = request.OrderId,
            UserId = 123 // Hardcoded for example
        };

        // Fetch user details
        var userRequest = new UserRequest { UserId = order.UserId };
        var user = await _userServiceClient.GetUserAsync(userRequest);

        // Fetch product details
        var productRequest = new ProductRequest { ProductId = 1 };
        var product = await _productServiceClient.GetProductAsync(productRequest);
        order.Products.Add(new Product
        {
            ProductId = product.ProductId,
            Quantity = 2
        });

        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode
Product Service
public class ProductService : ProductService.ProductServiceBase
{
    public override Task<ProductResponse> GetProduct(ProductRequest request, ServerCallContext context)
    {
        // Simulating a product record
        var product = new ProductResponse
        {
            ProductId = request.ProductId,
            ProductName = "Laptop",
            Price = 999.99f
        };
        return Task.FromResult(product);
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Client-Server Interaction

Now, any of these services can directly call the methods on the others using gRPC clients, making it easy to pull together all the data needed for a comprehensive response to the user.

Benefits and Drawbacks of RPC

Benefits

  • Low Latency: RPC calls are typically faster than traditional HTTP requests.
  • Simplicity: Calls are treated like local functions, reducing the need to handle HTTP methods, status codes, etc.
  • Strong Typing: With Protocol Buffers, data types are defined, leading to fewer data format issues.

Drawbacks

  • Tight Coupling: Services using RPC are often more tightly coupled since they rely directly on one another's methods.
  • Error Handling: If one service fails, it can be harder to handle errors without affecting the entire request chain.
  • Scaling Challenges: RPC can complicate scaling and load balancing since calls are synchronous.

RPC is a powerful tool for microservices communication, especially when low latency and synchronous calls are required. However, it’s crucial to weigh its advantages against potential downsides, such as tight coupling and error handling difficulties. For applications requiring high inter-service communication, RPC can be an effective choice, as long as the architecture is prepared to handle its complexities.

Top comments (0)