gRPC: understanding modern inter-process communication API

A microservices-based software system requires applications to talk to each other using an inter-process communication mechanism. gRPC is a modern inter-process communication system that is scalable and more efficient than the tradition RESTFul services.

Why gRPC

Typically, all modern software system comprises of a collection of applications distributed across networks, that communicate with each other by passing messages. One of the most popular ways of building a modern software system is by using a microservices architecture.

In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.

James Lewis and Martin Fowler

A microservices-based software system requires applications to talk to each other using an inter-process communication mechanism. Microservices can communicate with each other as

  • Synchronous communication (request-reply): In this pattern, a service calls an API exposed by another service and waits for the response. The API must have well-defined semantics and versioning.
  • Asynchronous communication: In this pattern, a service sends a message, without waiting for a response from another service. This message can be processed by one or more services.

A most common way of implementing a request-reply style of communication is by using RESTFul services, typically implemented as JSON payload over HTTP. However, RESTFul services can be quite bulky, inefficient, and limiting for lots of use cases. The bulkiness of RESTFul services comes from fact that most implementation relies on JSON, which is a text-based encoding format. The JSON format uses lots of space compared to the binary format. There are binary encoding formats of JSON (MessagePack, BSON, SMILE, etc.) but their adoption is not very widespread.

Another issue with using JSON based API is that the support of schema is optional. The most common way for an application to advertise API is by using OpenAPI spec but this is not tightly integrated with RESTFul architecture style. It’s very common to see OpenAPI spec and implementation drifting in due course.

To solve issues associated with RESTFul services, we need a modern inter-process communication system that is scalable and more efficient than RESTFul services. This is where gRPC comes into the picture.

The gRPC framework is based on binary encoding format protocol-buffer and implemented on top of HTTP/2. It’s strongly typed, polyglot, and provides both client and server-side streaming.

In gRPC, a client application can directly call a method on a distributed server application on a different machine as if it were a local object.

So, the question is – should you move all our APIs to use gRPC?

Probably No !!

Before we fully answer why let’s understand some facts about APIs. There are two types of APIs:

  • Public API: You publish API that is consumed by client applications typically browser, mobile application, or you are GitHub.
  • Private API: Your APIs are not exposed to the outside world and it’s mainly used for inter-service communication within your domain.

The de facto standard for using Public API is the REST over HTTP. For private APIs, think of using gRPC if you are okay with trade-offs associated with introducing a new technology stack vs all the goodies gRPC brings. Moreover, supporting gRPC on a browser-based application is not straightforward.

One of the possible gRPC based architecture can be to have API Gateway/BFF in front of the services and make all internal communication using gRPC.


The gRPC Advantage

Time for a sales pitch !!

The gRPC brings lots of advantages and that’s why it’s widely adopted by companies like Netflix and Dropbox.

Faster

The gRPC is much faster compared to traditional JSON-based REST APIs. The gRPC is based on binary protocol-buffer format which is very lightweight compared to text-based formats such as JSON.

For example, for below payload JSON encoding takes 81 bytes and the same payload in protocol-buffers takes 33 bytes (source – Chapter 4 – Designing Data-intensive Applications by Martin Kelpmann. If you haven’t read this book you have missed something !!)


{
	"serName": "Martin",
	"favoriteNumber": 1337,
	"interests": [" daydreaming", "hacking"]
}

It’s possible to implement REST on top of protocol-buffer but why would you want to do that?

Another reason which makes gRPC faster is that it’s implemented on top of HTTP/2. How fast? Check this comparison of HTTP/1.1 vs HTTP/2.

Strong type and well-defined interface

gRPC introduces the concept of a well-defined service interface and strong type which helps us avoid run time error. For the client application, it’s as good as calling the local method.

Polyglot

gRPC supports multiple programming languages. You can choose to write micro-services in the programming language you deemed fit and not worry about interoperability issues.

Stream support

gRPC supports both client and server-side streaming. Enabling these features in your application is as simple as changing the service definition.

Other features

gRPC has in-built support for resiliency, authentication, encryption, compression, load-balancing, and error handling. The gRPC is a proud member of Cloud Native Computing Foundation (CNCF) and many modern frameworks provide native support of gRPC.


Code sample

Enough of theory !! Let’s get our hands dirty by writing some code samples. The working code example of this article is listed on GitHub .


Getting started

Before we start let’s try to understand some important concepts. A gRPC application consists of three important components:

  • Protocol Buffers: It defines messages and services exposed by the server application
  • Server: The server application implements and exposes RPC services which can be called by the client application.
  • Client: The client application consumes RPC services exposed by the server.

Protocol Buffers

So what are Protocol Buffers ?

Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

Google

Protocol buffers are nothing but interface definition language (IDL) used to define API contracts in gRPC.

Let’s try to understand using an example from the e-commerce domain. In this example, the order-service (gRPC client) calls the product-service (gRPC server) to fetch information about the product by passing SKU (Stock keeping unit). A sample protocol buffer can be defined as :


syntax = "proto3";
package product;

option java_package = "dev.techdozo.product.api";

message ProductApiRequest {
  string sku = 1;
}

message ProductApiResponse {
  string name = 1;
  string description = 2;
  double price = 3;
}

service ProductApiService {
  rpc getProduct(ProductApiRequest) returns (ProductApiResponse);
}

The product-service exposes RPC API getProduct(ProductApiRequest) returns (ProductApiResponse), which is consumed by the order-service to get information about the product.

Let’s focus on the different constructs of the proto file.

Message

The message is a binary data structure that is exchanged between client and server.

Service

A service is a remote method that is exposed to the client. The client can use the generated stub to call the remote method on the server. In the above example, product-service exposes an RPC method getProduct(ProductApiRequest) returns (ProductApiResponse) which returns information about the Product as ProductApiResponse.

Code Generation

You can use protoc compiler to generate client and server code. The protoc compiler supports code generation in many different languages.

For our example, we will use Protobuf Gradle Plugin to generate source code in java. Protocol Buffer plugin assembles the Protobuf Compiler (protoc) command line and use it to generate Java source files out of the proto files. The generated java source files should be added in the sourceSet so that they can be compiled along with Java sources.


sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}

Running command gradlew build generates source code in the directory build/generated/source/proto/main/grpc and build/generated/source/proto/main/java.

gRPC Server

The gRPC server implements services defined in the proto files and exposes those as RPC API.

Implementing service definition

Now we have the Java project ready with autogenerated skeleton code. Let’s implement the service interface ProductApiService, as defined in the product.proto file.

To implement the business logic override getProduct(..) method in the autogenerated abstract class ProductApiServiceGrpc.ProductApiServiceImplBase.


public class ProductApiService extends ProductApiServiceGrpc.ProductApiServiceImplBase {

  private ProductRepository productRepository;

  public ProductApiService() {
    this.productRepository = new ProductRepository();
  }

  @Override
  public void getProduct(
      ProductApiRequest request, StreamObserver<ProductApiResponse> responseObserver) {

    log.info("Calling Product Repository..");

    String sku = request.getSku();
    Optional<ProductInfo> productInfo = productRepository.get(sku);

    if (productInfo.isPresent()) {
      ProductInfo product = productInfo.get();
      ProductApiResponse productApiResponse =
          ProductApiResponse.newBuilder()
              .setName(product.getName())
              .setDescription(product.getDescription())
              .setPrice(product.getPrice())
              .build();
      responseObserver.onNext(productApiResponse);
      responseObserver.onCompleted();
    } else {
      responseObserver.onError(new StatusException(Status.NOT_FOUND));
    }

    log.info("Finished calling Product API service..");
  }
}

The above code fetches Product information from ProductRepository and calls onNext() on StreamObserver by passing the ProductApiResponse. The onCompleted() method notifies the stream about successful completion.

In case of error, you can call onError() method by passing the appropriate error code.

Implementing Server code

Exposing service for client applications to use, is as simple as creating a gRPC server instance and registering the ProductApiService service to the server. The server listens on the specified port and dispatches all requests to the relevant service.


public ProductServer(int port) {
  this.port = port;
  ProductApiService productApiService = new ProductApiService();
  this.server = ServerBuilder.forPort(port).addService(productApiService).build();
}

gRPC Client

The first thing we need to do to implement a gRPC client is to generate client stubs using the proto file defined in the server (product-service) application.


public class ProductClient {

  private final ManagedChannel managedChannel;
  private final ProductApiServiceGrpc.ProductApiServiceBlockingStub productApiServiceBlockingStub;

  public ProductClient(String host, int port) {
    this.managedChannel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build();
    this.productApiServiceBlockingStub = ProductApiServiceGrpc.newBlockingStub(this.managedChannel);
  }

  public void call() {
    log.info("Calling Server..");

    ProductApiRequest productApiRequest =
        ProductApiRequest.newBuilder().setSku("apple-123").build();

    ProductApiResponse product = productApiServiceBlockingStub.getProduct(productApiRequest);

    log.info("Received Product information from product service, info {}", product);
  }

  public static void main(String[] args) {
    ProductClient client = new ProductClient("0.0.0.0", 8080);
    client.call();
  }
}

You can create a gRPC channel specifying the server address and port as ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(). The channel represents a virtual connection to an endpoint to perform RPC.

You can create the client stub using the newly created channel as:


ProductApiServiceGrpc.ProductApiServiceBlockingStub 
productApiServiceBlockingStub = ProductApiServiceGrpc.newBlockingStub(
    ManagedChannelBuilder.forAddress(host, port).usePlaintext().build());

There are two types of client stubs:

  • Blocking: The BlockingStub, which waits until it receives a server response.
  • Non Blocking: The NonBlockingStub, which doesn’t wait for server response, but instead registers an observer to receive the response.

Here plaintext means we are setting up an unsecured connection between client and server.

That’s it !! Now you have one simple gRPC application ready.


Summary

An application based on gRPC can be a great choice for inter-process communication. The gRPC services are faster compared to RESTFul services. Another benefit of gRPC is a well-defined service interface and strong type. The gRPC services communicate using Protocol Buffer, a binary format for exchanging data. The gRPC is supported in many programming languages. Code generation tools can be used to generate client and server stubs in the gRPC.

Leave a Reply