API first with gRPC

Introduction

This article is a continuation of the previous article on API first implementation. Here, we will see API first implementation with gRPC in action. The entire source code is on GitHub. I expect the readers to have at least some knowledge of gRPC and protobuf in this article.

gRPC

gRPC is a remote procedure call framework that works across languages. It is specifically designed for service to service communication. There are tons of getting started examples and definitions on gRPC. In the last article, we saw how to break the existing monolith structure and to decouple separate services out of it. My main focus here would be to demonstrate how to evolve the existing code base and make it RPC ready.

Although not part of this article, but one done, we can deploy these services separately on different machines and they can communicate via RPC.

The existing system

Let us first review the existing structure of our app (git checkout modules-api)

Existing API first implementation

As we can see, we still have a runtime dependency on the inventory module. This means that we still cannot separate our systems physically. However since RPC calls are over the network, we can make sure the physical separation as well.

proto definitions

First, let us have a look at the proto definions.

message InventoryStatus {
  int64 unique_item_id = 1;
  string item_name = 2;
  string item_description = 3;
  int32 available_quantity = 4;
}

message InventoryStatusRequest {
  int64 item_id = 1;
}

service InventoryService {
  rpc getStatus(InventoryStatusRequest) returns (InventoryStatus);
}
  

There is only one service called InventoryService. The gRPC plugin will automatically generate the service definitions and the DTOs automatically.

gRPC server

This is how our server looks like

The GrpcAPIServer is our gRPC server which listens on a specific port, 50000 in our case. It is recommended to use a port between 50000 and 59999 for RPC calls.

public class GrpcApiServer {

    private final BindableService service;

    public GrpcApiServer(BindableService service) {
        this.service = service;
        registerService();
    }

    private void registerService() {
        ServerServiceDefinition serverServiceDefinition = service.bindService();
        ServerBuilder<?> serverBuilder = ServerBuilder.forPort(50000);
        Server server = serverBuilder.addService(serverServiceDefinition).build();
        startGrpcServer(server);
    }

    private void startGrpcServer(Server server) {
        try {
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  

The gRPC server has the knowledge of the API that it serves, in our case it is the auto-generated file InventoryServiceGrpc. We need to extend this auto-generated file in order to serve the request. The BindableService above is nothing but an instance of InventoryServiceApiGrpc class.

public class InventoryServiceApiGrpc extends InventoryServiceGrpc.InventoryServiceImplBase {

    private final InventoryService service;
    public InventoryServiceApiGrpc(InventoryService service) {
        this.service =  service;
    }

    @Override
    public void getStatus(InventoryStatusRequest request,
                          StreamObserver<InventoryStatus> responseObserver) {
        responseObserver.onNext(service.getStatus(request));
        responseObserver.onCompleted();
    }
}
  

The InventoryServiceApiGrpc simply acts a gateway and forward the requests to InventoryService to serve the request.

gRPC client

As we can see from the below figure, even the client has the knowledge of the auto-genareted InventoryServiceGrpc file.

However, the client only needs the interface definition and does not care about the implementation. It just needs to know which methods to call.

public class GrpcApiClient {

    private AbstractStub stub;
    public GrpcApiClient(Class<?> grpc) {
        ManagedChannelBuilder<?> channelBuilder = ManagedChannelBuilder.forAddress("localhost", 50000);
        channelBuilder.usePlaintext();

        ManagedChannel channel = channelBuilder.build();
        try {
            Class<?> grpcClass = grpc.forName(grpc.getCanonicalName());
            Method method = findMethodOnClass("newBlockingStub", grpcClass);
            this.stub = (AbstractStub) method.invoke(grpcClass, channel);
        } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException("Cannot create gRPC client", e);
        }
    }

    private static Method findMethodOnClass(String methodName, Class<?> grpcClass) {
        Method[] methods = grpcClass.getMethods();
        Method method = null;

        for (Method m : methods) {
            if (m.getName().equals(methodName)) {
                method = m;
                break;
            }
        }

        if (method == null) {
            throw new RuntimeException("Cannot find methodName method");
        }

        return method;
    }

    public AbstractStub getStub() {
        return this.stub;
    }
}
  

The Class<?> grpc above is nothing but a reference of InventoryServiceGrpc.class.

This completes the setup of our RPC communication. Now the next step is to establish a connection between the two.

The connection

The order module is the real consumer of the inventory module. The common connection between them is the InventoryService interface. The inventory module provides an actual implementation of the interface, which in turn is utilized by the InventoryServiceApiGrpc class.

For the consumption, the order module again implements the same InventoryService interface. But instead of providing a real implementation, it actually wraps the GrpcApiClient and delegates the call to it.

public class GrpcApiClient {
 
    private AbstractStub stub;
    public GrpcApiClient(Class<?> grpc) {
        ManagedChannelBuilder<?> channelBuilder = ManagedChannelBuilder.forAddress("localhost", 50000);
        channelBuilder.usePlaintext();
 
        ManagedChannel channel = channelBuilder.build();
        try {
            Class<?> grpcClass = grpc.forName(grpc.getCanonicalName());
            Method method = findMethodOnClass("newBlockingStub", grpcClass);
            this.stub = (AbstractStub) method.invoke(grpcClass, channel);
        } catch (ClassNotFoundException | IllegalAccessException | InvocationTargetException e) {
            throw new RuntimeException("Cannot create gRPC client", e);
        }
    }
 
    private static Method findMethodOnClass(String methodName, Class<?> grpcClass) {
        Method[] methods = grpcClass.getMethods();
        Method method = null;
 
        for (Method m : methods) {
            if (m.getName().equals(methodName)) {
                method = m;
                break;
            }
        }
 
        if (method == null) {
            throw new RuntimeException("Cannot find methodName method");
        }
 
        return method;
    }
 
    public AbstractStub getStub() {
        return this.stub;
    }
}
  

The client in turn calls the GrpcAPIServer, which already has a reference of the InventoryServiceApiGrpc class. This way, the call is transparent to the order module and it looks like it is making a local call instead.

Conclusion

In this article, we saw how we can leverage the gRPC to make calls through the network while making everything transparent to the consumers. This will also help us in physically separating the services and make our services efficient when compared to HTTP/JSON based RESTful services.

Leave a Reply

Your email address will not be published. Required fields are marked *