Microservice application with gRPC using Java and Spring Boot

Shivraj Sonth
Javarevisited
Published in
21 min readMay 19, 2023

Recently I have been digging into Microservices communication mechanisms and gRPC was something that caught my attention. gRPC(Google Remote Procedure Call) has gained popularity in the microservice world because of the efficiency, security, and performance benefits it brings to client-server applications. You can refer here for details on what is gRPC and its benefits. I decided to try my hands on gRPC and provide a step-by-step process on how to develop a simple client-server application using gRPC protocol. Let’s get started!!

Pre-requisites

  1. Java
  2. Spring Boot
  3. RESTful Webservices
  4. Understanding of gRPC protocol, protobuf messages, etc.

Use case

Let’s consider a simple use case where the requirement is to create a Microservices backend to store user data in a file format and allow the user to read and update when required. The backend should support storing the data in both CSV and XML file formats.

There is a small interesting story behind this use case, which led to me writing this blog. Check it out in the last section.

The application should contain 2 Microservices:

Client Service

  1. This will be a consumer-facing service which will accept REST requests from the user.
  2. The service should accept the data in JSON format within the request body.
  3. The service should also accept another input fileType as a header within the request. The value of this parameter could be either CSV or XML. This is only used for POST requests.
  4. The service should expose 3 endpoints as below:
  • GET( /grpc/users/{id}): To read existing data.
  • POST( /grpc/users/): To create new data (Requires a Request Body).
  • PUT ( /grpc/users/{id}).: To update existing data (Requires a Request Body).

Sample body:

{   
id: 1234,
name: "Dev",
dob: "20–08–2020",
salary: "122111241.150"
}

Request Header or Parameter:

fileType = CSV

fileType = XML

Backend Service :

This is a backend gRPC service that will be sitting on the server side. This service will not be exposed to the outside world. “Client Service” sits between the “Consumer” and the “Backend Service ” to issue necessary instructions for all the CRUD operations. The Client Service communicates with Backend Service through gRPC calls.

(NOTE: There are several gRPC communication mechanisms like Simple RPC, Server-Streaming RPC, Client Streaming RPC, and Bi-directional Streaming RPC. For our use case we use the Simple RPC mechanism)

Check out the architecture diagram below:

Architecture

Project Setup

We will set up three projects as depicted in the below picture

Project Setup (Image Source: https://yidongnan.github.io/grpc-spring-boot-starter/en/server/getting-started.html#project-setup )
  1. Interface Project ( grpc-common ): This is the interface project which will contain all the common .proto files.
    NOTE: This project will be a dependency on the Server and Client project
  2. Client Project ( grpc-client ): Client project which is consumer-facing. It uses gRPC stubs to access to communicate with the server
  3. Server Project ( grpc-server ): Server project which is the backend server in our case which has the actual implementation for all the CRUD operations

Steps to Setup

You can find the working code example for this use case here: Microservice communication using gRPC

Use https://start.spring.io/ to create 3 maven projects namely

  1. grpc-common
  2. grpc-server
  3. grpc-client

Encapsulate all the above three projects inside a single parent project named microservices-grpc. Use any IDE like IntelliJ and open the microservivces-grpc project

Refer to the below image for the project structure. Ensure that you set it up similarly for smooth execution further. Check the maven dependencies section for all the pom.xml files:

Maven Dependencies (pom.xml)

grpc-common

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microservices.grpc</groupId>
<artifactId>microservices-grpc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<groupId>com.microservices.grpc</groupId>
<artifactId>grpc-common</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.30.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.30.0</version>
</dependency>


</dependencies>

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>
com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>
io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier}
</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>

grpc-client

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microservices.grpc</groupId>
<artifactId>microservices-grpc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<groupId>com.microservices.grpc</groupId>
<artifactId>grpc-client</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.microservices.grpc</groupId>
<artifactId>grpc-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-client-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.21.5</version>
<scope>compile</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>



<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>

<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.2.11</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>

grpc-server

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.microservices.grpc</groupId>
<artifactId>microservices-grpc</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>

<groupId>com.microservices.grpc</groupId>
<artifactId>grpc-server</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-starter</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/net.devh/grpc-server-spring-boot-autoconfigure -->
<dependency>
<groupId>net.devh</groupId>
<artifactId>grpc-server-spring-boot-autoconfigure</artifactId>
<version>2.14.0.RELEASE</version>
</dependency>

<dependency>
<groupId>com.microservices.grpc</groupId>
<artifactId>grpc-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.2.11</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-csv</artifactId>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.21.5</version>
<scope>compile</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.0</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>

</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>


</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Parent Project: microservices-grpc

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.microservices.grpc</groupId>
<artifactId>microservices-grpc</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>microservices-grpc</name>
<description>Microservice Communication using gRPC</description>
<modules>
<module>grpc-common</module>
<module>grpc-server</module>
<module>grpc-client</module>
</modules>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Code

In order to keep things simple, I will explain only the important parts of the code which constitute the end-to-end flow of the request-response sequence. You can refer to the code for more details. Have written appropriate comments for easy understanding.

1. Interface project (grpc-common)

Create a proto file: src/main/proto/common.proto in grpc-common project. This file contains all the protobuf messages and service declarations.

(NOTE: The proto files must be under src/main/proto/ directory. Otherwise, the files won't be detected as proto files by the protoc compiler and they may not compile properly).

You can see in the above file that, we have the UserService which has three rpc methods :

  1. getUser method which takes in a GetUserRequest message and returns a User message. This corresponds to GET request implementation.
  2. createUser method which takes in a CreateOrSaveUserRequest message and returns a CreateOrSaveUserResponse message. This corresponds to the POST request implementation
  3. saveUser method which also takes in a CreateOrSaveUserRequest message and returns a CreateOrSaveUserResponse message. This corresponds to the PUT request implementation

ErrorDetail message forms a common message format in case of any errors or exceptions.

Compile and generate sources

Click on the Maven tab in IntelliJ in the top right corner -> Go to grpc-common -> Lifecycle -> Click compile.

Maven Compile

Ensure the compilation is successful

Successful compilation message

You can see the generated source files in the target directory as shown below.

NOTE: Ensure you mark the following directories: target/generated-sources/protobuf/grpc-java , target/generated-sources/protobuf/java as generated sources.

Generated sources

2. Client project (grpc-client)

As discussed earlier, this is the consumer-facing application that accepts REST API calls. Let's define the Application, Controller, and Service classes.

Spring Boot Application Class :

This will start the web container on port 8080.

src/main/java/com/microservices/grpc/GrpcClientApplication.java

package com.microservices.grpc;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GrpcClientApplication {
public static void main(String[] args){
SpringApplication.run(GrpcClientApplication.class, args);
}

}

Client Controller and Validations

The controller exposes endpoint: /grpc/users/ for GET, PUT and POST requests. The controller has validations for the input parameter(id) and request body(UserPojo). You can see them via @NotNull, @Positive, and @Valid annotations. spring-boot-starter-validation module makes it easy to validate the parameters for some basic constraints automatically without having to write the validation code on our own.

src/main/java/com/microservices/grpc/controller/UserClientController.java

package com.microservices.grpc.controller;

import com.microservices.grpc.exceptions.ErrorCode;
import com.microservices.grpc.exceptions.InvalidArgumentException;
import com.microservices.grpc.pojo.Response;
import com.microservices.grpc.pojo.UserPojo;
import com.microservices.grpc.service.UserClientService;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import java.io.IOException;
import java.net.URI;
import java.util.Map;

@RestController
@RequestMapping("/grpc")
@AllArgsConstructor
@Log4j2
@Validated
public class UserClientController {

@Autowired
UserClientService userClientService;

@GetMapping("/users/{id}")
public ResponseEntity<Object> get(@PathVariable("id") @NotNull @Positive(message = "User id must be greater than 0") int id) throws IOException {
log.info("Received request for user with id: {}", id);
return ResponseEntity.ok(userClientService.getUser(id));
}

@PostMapping(value = "/users/", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Object> create(@RequestHeader(value = "fileType", defaultValue = "CSV") String fileType, @Valid @RequestBody() UserPojo userPojo) throws IOException {
log.info("Received PUT request for user with id: {}", userPojo.getId());
log.debug("Json String: {}", userPojo.toString());
fileType = fileType.equalsIgnoreCase("XML") ? fileType.toLowerCase() : "csv";
Response response = userClientService.createUser(userPojo, fileType);
return ResponseEntity.created(URI.create(String.format("/grpc/users/%s", response.getId()))).body(response);
}

@PutMapping(value = "/users/{id}", consumes = {MediaType.APPLICATION_JSON_VALUE}, produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseEntity<Object> save(@PathVariable @NotNull @Positive(message = "User id must be greater than 0") int id, @Valid @RequestBody() UserPojo userPojo) throws IOException {
log.info("Received PUT request for user with id: {}", id);
log.debug("Json String: {}", userPojo.toString());
if (id != userPojo.getId()) {
throw new InvalidArgumentException(ErrorCode.BAD_ARGUMENT.getMessage(), Map.of("parameter", "id", "message", "Value of variable: id must be same in both path param and request body."));
}

Response response = userClientService.saveUser(userPojo);
return ResponseEntity.ok(response);
}
}

UserPojo class is the Java representation of the User protobuf message which we will need for our internal manipulations.

src/main/java/com/microservices/grpc/pojo/UserPojo.java

package com.microservices.grpc.pojo;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.*;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@Getter
@Setter
@XmlRootElement(name = "user")
@XmlAccessorType(XmlAccessType.FIELD)
public class UserPojo {

@NotNull(message = "Id cannot be null")
@Positive(message = "Id should be a positive number")
@XmlElement(name = "id")
private int id;

@NotEmpty(message = "Name cannot be empty")
@NotBlank(message = "Name cannot be blank")
@XmlElement(name = "name")
@Pattern(regexp = "^[a-zA-Z]+(?:\\s+[a-zA-Z]+)*$", message = "Name must contain only alphabets along with white spaces.")
private String name;


@Pattern(regexp = "^\\d{4}\\-(0[1-9]|1[012])\\-(0[1-9]|[12][0-9]|3[01])$", message = "DOB must of pattern: yyyy-mm-dd")
@XmlElement(name = "dob")
private String dob;

@PositiveOrZero(message = "Salary should be greater than or equal to 0")
@XmlElement(name = "salary")
private double salary;


@Override
public String toString() {
return "{" +
"id: " + id +
", name:'" + name + '\'' +
", dob: '" + dob + '\'' +
", salary: " + salary +
'}';
}
}

Client Service

The controller uses UserClientService as the service class, which performs the action of delegating the request to the backend gRPC Server and getting the response.

In the UserClientService class above, we use the UserServiceGrpc.UserServiceBlockingStub . This is a client stub that gets generated when we compile the common.proto file in the grpc-commmon project.

@GrpcClient(“grpc-service”) annotation helps us to create a gRPC channel with the name: grpc-service using the client stub. The UserClientService makes the RPC calls (synchronousClient.getUser(getUserRequest) , synchronousClient.createUser(createOrSaveUserRequest), synchronousClient.saveUser(createOrSaveUserRequest) ) via the gRPC channel and gets the response back in protobuf format.

src/main/java/com/microservices/grpc/service/UserClientService.java

package com.microservices.grpc.service;

import com.microservices.grpc.*;
import com.microservices.grpc.mapper.ServiceExceptionMapper;
import com.microservices.grpc.pojo.Response;
import com.microservices.grpc.pojo.UserPojo;
import com.microservices.grpc.util.CommonUtility;
import io.grpc.StatusRuntimeException;
import lombok.extern.log4j.Log4j2;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
@Log4j2
public class UserClientService {

@GrpcClient("grpc-service")
UserServiceGrpc.UserServiceBlockingStub synchronousClient;
@Autowired
CommonUtility convertUtil;


public UserPojo getUser(int id) throws IOException {
log.info("Processing GET request for user id: {}", id);
GetUserRequest getUserRequest = GetUserRequest.newBuilder().setId(id).build();
User responseUser;
try {
responseUser = synchronousClient.getUser(getUserRequest);
} catch (StatusRuntimeException error) {
log.error("Error while getting user details, reason {} ", error.getMessage());
throw ServiceExceptionMapper.map(error);
}
log.debug("Got response: {}", responseUser.toString());
return convertUtil.convertToPojo(responseUser);

}

public Response createUser(UserPojo userPojo, String fileType) throws IOException {
User user = convertUtil.convertToProtoBuf(userPojo);
CreateOrSaveUserRequest createOrSaveUserRequest = CreateOrSaveUserRequest.newBuilder().setUser(user).setFileType(fileType).build();
CreateOrSaveUserResponse createOrSaveUserResponse ;
try {
createOrSaveUserResponse = synchronousClient.createUser(createOrSaveUserRequest);
} catch (StatusRuntimeException error) {
log.error("Error while creating user, reason {} ", error.getMessage());
throw ServiceExceptionMapper.map(error);
}
log.info("Successfully created new user. id: {}", createOrSaveUserResponse.getId());
return convertUtil.getResponse(createOrSaveUserResponse);

}

public Response saveUser(UserPojo userPojo) throws IOException {
log.info("Processing request for user id: {}", userPojo.getId());
User user = convertUtil.convertToProtoBuf(userPojo);
CreateOrSaveUserRequest createOrSaveUserRequest = CreateOrSaveUserRequest.newBuilder().setUser(user).build();
CreateOrSaveUserResponse createOrSaveUserResponse;
try {
createOrSaveUserResponse = synchronousClient.saveUser(createOrSaveUserRequest);
} catch (StatusRuntimeException error) {
log.error("Error while saving user, reason {} ", error.getMessage());
throw ServiceExceptionMapper.map(error);
}
log.info("Successfully saved user. id: {}", createOrSaveUserResponse.getId());
return convertUtil.getResponse(createOrSaveUserResponse);

}


}

The gRPC channel connection details are provided in application.yaml.

NOTE: grpc-service.address property is the address of the backend gRPC server. Since I am running my gRPC server on my local machine on port 9000, I have given the address as static://localhost:9000.

src/main/resources/application.yaml

grpc:
client:
grpc-service:
address: static://localhost:9000
negotiationType: plainText

Exception Handling

GlobalExceptionHandler.java handles all the different kinds of exceptions at a commonplace. Using @ControllerAdvice and @ExceptionHandler we can intercept all exceptions and construct a common error message (ErrorResponse)with appropriate details so that the consumer can interpret them properly.

src/main/java/com/microservices/grpc/hanlder/GlobalExceptionHandler.java

package com.microservices.grpc.handler;

import com.microservices.grpc.exceptions.ErrorCode;
import com.microservices.grpc.exceptions.ErrorResponse;
import com.microservices.grpc.exceptions.InvalidArgumentException;
import com.microservices.grpc.exceptions.ServiceException;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

@ControllerAdvice
@Log4j2
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
var errorResponse = new ErrorResponse();
errorResponse.setErrorCode(ErrorCode.BAD_ARGUMENT);
errorResponse.setMessage(ErrorCode.BAD_ARGUMENT.getMessage());
HashMap validationErrors = new HashMap<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
validationErrors.put(fieldError.getField(), fieldError.getDefaultMessage());
}
errorResponse.setDetails(validationErrors);
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(ServiceException.class)
public ResponseEntity<ErrorResponse> handleServiceException(ServiceException cause) {
log.error("Exception occured: {}", cause);
return buildServiceErrorResponse(cause);
}

@ExceptionHandler(ConstraintViolationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ResponseEntity<ErrorResponse> handleConstraintViolationException(ConstraintViolationException e) {
var errorResponse = new ErrorResponse();
errorResponse.setErrorCode(ErrorCode.BAD_ARGUMENT);
errorResponse.setMessage(e.getMessage());
errorResponse.setDetails(Map.of("errors", e.getConstraintViolations().stream().map((cv) -> {
return cv == null ? "null" : cv.getPropertyPath() + ": " + cv.getMessage();
}).collect(Collectors.joining(", "))));
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex) {
var errorResponse = new ErrorResponse();
errorResponse.setErrorCode(ErrorCode.BAD_ARGUMENT);
errorResponse.setMessage("Invalid parameter value. Parameter name: " + ex.getName());
errorResponse.setDetails(Map.of("parameterName", ex.getName(), "parameterValue", ex.getValue().toString(), "cause", ex.getCause().toString()));
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(InvalidArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorResponse> handleInvalidArgumentException(InvalidArgumentException exception) {
var errorResponse = new ErrorResponse();
errorResponse.setErrorCode(ErrorCode.BAD_ARGUMENT);
errorResponse.setMessage(exception.getMessage());
errorResponse.setDetails(exception.getErrorMetaData());
return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
}


@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<ErrorResponse> handleAllUncaughtException(Exception exception) {
log.error("Unknown error occurred", exception);
return buildErrorResponse(exception);
}

private ResponseEntity<ErrorResponse> buildErrorResponse(
Exception cause
) {
var errorResponse = new ErrorResponse();
errorResponse.setErrorCode(ErrorCode.INTERNAL_SERVER_ERROR);
errorResponse.setMessage(cause.getMessage());
errorResponse.setDetails(Map.of("cause", cause.getCause().toString()));
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

private ResponseEntity<ErrorResponse> buildServiceErrorResponse(
ServiceException cause
) {
var errorResponse = new ErrorResponse();
var errorCode = cause.getErrorCode();
errorResponse.setErrorCode(errorCode);
errorResponse.setMessage(cause.getMessage());
errorResponse.setDetails(cause.getErrorMetaData());
return new ResponseEntity<>(errorResponse, errorCode.getHttpStatus());
}

}

3. Backend Service (grpc-server)

This is the backend gRPC server, which performs the actual CRUD operation. Let’s write the implementation classes

Backend Service Implementation

Firstly, we need to annotate the service class with @GrpcService. @GrpcService annotation signifies that the class has the implementation of the services defined in the proto files. These implementations are then exposed as RPC API.

The grpc service class i.e. UserServerService class has to extend the autogenerated abstract class UserServiceGrpc.UserServiceImpl and implement the services by overriding the service methods (getUser, createUser, and saveUser).

src/main/java/com/microservices/grpc/service/UserServerService.java

package com.microservices.grpc.service;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.microservices.grpc.*;
import com.microservices.grpc.constants.CommonConstants;
import com.microservices.grpc.exceptions.ErrorCode;
import com.microservices.grpc.exceptions.FileAlreadyExistsException;
import com.microservices.grpc.exceptions.ResourceNotFoundException;
import com.microservices.grpc.pojo.UserPojo;
import com.microservices.grpc.util.FileUtility;
import com.microservices.grpc.util.CommonUtility;
import io.grpc.stub.StreamObserver;
import lombok.extern.log4j.Log4j2;
import net.devh.boot.grpc.server.service.GrpcService;
import org.springframework.beans.factory.annotation.Autowired;

import javax.xml.bind.JAXBException;
import java.io.IOException;
import java.util.Map;


@GrpcService
@Log4j2
public class UserServerService extends UserServiceGrpc.UserServiceImplBase {



@Autowired
FileUtility fileUtil;

@Autowired
CommonUtility converterUtil;

@Override
public void getUser(GetUserRequest request, StreamObserver<User> responseObserver) {
/*
- Logic is to store all the files with id as name with its respective extension
- Whenever a get requests is received by the server, search for the file name with id
- If it exists, parse the csv/xml into a pojo
- If not, throw ResourceNotFoundException
- Convert the POJO to protobuf format
- Send the response
*/
String id = String.valueOf(request.getId());
UserPojo userPojo = fileUtil.findById(id);
if(userPojo == null){
throw new ResourceNotFoundException("Resource not found.", Map.of("id", id, "message", "Resource Not Found"));
}
User responseUser = null;
try {
responseUser = converterUtil.convertToProtoBuf(userPojo);
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
responseObserver.onNext(responseUser);
responseObserver.onCompleted();
}


/* - Assuming the request body is validated for the correctness of the input parameters, check if there is already an existing file with the given id present in either CSV/XML
- If present, throw error saying already exists
- If not, create the CSV/XML and send the success response saying created, with a proper success response as below
- {id: 1, path: /grpc/users/<id>, statusMessage: User created successfully}
*/
@Override
public void createUser(CreateOrSaveUserRequest createOrSaveUserRequest, StreamObserver<CreateOrSaveUserResponse> responseObserver) {
User userRequest = createOrSaveUserRequest.getUser();
String fileType = createOrSaveUserRequest.getFileType();
if(fileUtil.getFileNameForId(String.valueOf(userRequest.getId())) != null){
throw new FileAlreadyExistsException(ErrorCode.USER_ALREADY_EXISTS.getMessage(), Map.of("id", String.valueOf(userRequest.getId()), "message", ErrorCode.USER_ALREADY_EXISTS.getMessage()));
}
UserPojo userPojo;
CreateOrSaveUserResponse createOrSaveUserResponse = null;
try {
userPojo = converterUtil.convertToPojo(userRequest);
if(fileUtil.create(userPojo, fileType)){
createOrSaveUserResponse = CreateOrSaveUserResponse.newBuilder().setId(userPojo.getId()).setStatusMessage(CommonConstants.SUCCESSFUL_POST_MESSAGE).setPath(String.format("/grpc/users/%s", userPojo.getId())).build();
}

} catch (IOException | JAXBException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
responseObserver.onNext(createOrSaveUserResponse);
responseObserver.onCompleted();
}


/* - Assuming the request body is validated for the correctness of the input parameters, check if there is already an existing file with the given id present in either CSV/XML
- If present, update the existing file with new details
- If not, create the CSV/XML
- Send the success response as below
- {id: 1, path: /grpc/users/<id>, statusMessage: User updated successfully}
*/
@Override
public void saveUser(CreateOrSaveUserRequest createOrSaveUserRequest, StreamObserver<CreateOrSaveUserResponse> responseObserver) {
User userRequest = createOrSaveUserRequest.getUser();
UserPojo userPojo;
CreateOrSaveUserResponse createOrSaveUserResponse = null;

try {
userPojo = converterUtil.convertToPojo(userRequest);
if(fileUtil.save(userPojo)){
createOrSaveUserResponse = CreateOrSaveUserResponse.newBuilder().setId(userPojo.getId()).setStatusMessage(CommonConstants.SUCCESSFUL_PUT_MESSAGE).setPath(String.format("/grpc/users/%s", userPojo.getId())).build();
}
} catch (IOException | JAXBException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
responseObserver.onNext(createOrSaveUserResponse);
responseObserver.onCompleted();
}

}

UserServerService class has two members namely

  1. FileUtility fileUtil: This is a utility class that contains utility methods to find, create or update a CSV/XML file. Since, this application is all about saving and updating XML or CSV files, putting all of these utility functions in a single class is recommended.
  2. CommonUtility converterUtil: The gRPC server receives the request body from the client in protobuf format (CreateOrSaveUserRequest which in turn contains the User message). We will need the equivalent POJO representation of the protobuf message while creating or parsing the CSV files. Hence we have this CommonUtility class which contains methods to convert a POJO to protobuf and vice versa.

/src/main/java/com/microservices/grpc/util/FilieUtility.java

package com.microservices.grpc.util;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.dataformat.csv.CsvMapper;
import com.fasterxml.jackson.dataformat.csv.CsvSchema;
import com.microservices.grpc.config.FileStorageProperties;
import com.microservices.grpc.constants.CommonConstants;
import com.microservices.grpc.exceptions.FileParsingException;
import com.microservices.grpc.exceptions.FileStorageException;
import com.microservices.grpc.exceptions.ResourceNotFoundException;
import com.microservices.grpc.pojo.Response;
import com.microservices.grpc.pojo.UserPojo;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.io.FilenameUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;

@Log4j2
@Component
public class FileUtility {

private final Path fileStorageDir;

@Autowired
public FileUtility(FileStorageProperties fileStorageProperties) {
this.fileStorageDir = Path.of(fileStorageProperties.getStorageDir())
.toAbsolutePath().normalize();
try {
Files.createDirectories(this.fileStorageDir);
} catch (Exception ex) {
log.error("Error creating storage dir on the server. Exception: {}", ex);
throw new FileStorageException("Error occurred while creating the file storage directory.", Map.of("storage_dir", this.fileStorageDir.toAbsolutePath().toString(), "message", "Error occurred while creating the file storage directory.", "exception", ex.toString()));
}
}
public UserPojo findById(String id){
String fileName = getFileNameForId(id);
if(fileName == null){
return null;
}
UserPojo userPojo = parseFile(id, fileName);

return userPojo;

}

public String getFileNameForId(String id){

File directory = new File(fileStorageDir.toString());
FileFilter filter = new FileFilter(id + ".");
String[] fileList = directory.list(filter);

if (fileList == null || fileList.length == 0) {
log.info("Resource not found: Id: {}", id);
return null;
}

if(fileList.length > 1){
log.warn("Multiple files detected for the user with id: {}. This is not an expected behaviour. Pick the first occurrence of the file name.", id);
}
return Path.of(fileStorageDir.toString(),fileList[0] ).toAbsolutePath().toString();
}

public boolean create(UserPojo userPojo, String fileType) throws IOException, JAXBException {

if(fileType.toLowerCase().equals(CommonConstants.CSV_FILE_EXTENSION)){
createCSVFile(userPojo);
}else{
createXMLFile(userPojo);
}
return true;
}
public boolean save(UserPojo userPojo) throws IOException, JAXBException {

String fileName = getFileNameForId(String.valueOf(userPojo.getId()));
if(fileName != null) {
log.debug("Found existing user file: {}", fileName);
String extension = FilenameUtils.getExtension(fileName);
if (extension.equals(CommonConstants.CSV_FILE_EXTENSION)) {
createCSVFile(userPojo);
} else {
createXMLFile(userPojo);
}
} else{
createCSVFile(userPojo);
}
return true;
}

public UserPojo createCSVFile(UserPojo userPojo) throws IOException {
File csvOutputFile = new File(fileStorageDir.toString() + "\\" + userPojo.getId() + "." +CommonConstants.CSV_FILE_EXTENSION);
writeToCSV(userPojo, csvOutputFile);
log.info("Successfully created user file with id: {}", userPojo.getId());
return userPojo;

}

public boolean writeToCSV(UserPojo userPojo, File csvOutputFile) throws IOException {

CsvMapper mapper = new CsvMapper();
mapper.configure(JsonGenerator.Feature.IGNORE_UNKNOWN, true);

CsvSchema schema = CsvSchema.builder().setUseHeader(true)
.addColumn(CommonConstants.ID_COLUMN)
.addColumn(CommonConstants.NAME_COLUMN)
.addColumn(CommonConstants.DOB_COLUMN)
.addColumn(CommonConstants.SALARY_COLUMN)
.build();

ObjectWriter writer = mapper.writerFor(UserPojo.class).with(schema);

writer.writeValues(csvOutputFile).writeAll(Collections.singleton(userPojo));

return true;

}

public UserPojo createXMLFile(UserPojo userPojo) throws IOException, JAXBException {
File xmlOutputFile = new File(fileStorageDir.toString() + "\\" + userPojo.getId() + "." +CommonConstants.XML_FILE_EXTENSION);
writeToXML(userPojo, xmlOutputFile);
log.info("Successfully created user file with id: {}", userPojo.getId());
return userPojo;

}

public boolean writeToXML(UserPojo userPojo, File xmlOutputFile) throws IOException, JAXBException {
// create JAXB context and instantiate marshaller
JAXBContext context = JAXBContext.newInstance(UserPojo.class);
Marshaller m = context.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);

// Write to File
m.marshal(userPojo,xmlOutputFile);
return true;

}
public UserPojo parseFile(String id, String fileName){
UserPojo userPojo = null;
String extension = FilenameUtils.getExtension(fileName);
if(extension.equals(CommonConstants.CSV_FILE_EXTENSION)){
userPojo = parseCSVFile(id, fileName);
}else{

userPojo = parseXMLFile(id, fileName);
}
return userPojo;
}


public UserPojo parseCSVFile(String id, String fileName){
UserPojo userPojo = null;
try(
BufferedReader br = new BufferedReader(new FileReader(fileName));
CSVParser parser = CSVFormat.DEFAULT.withDelimiter(',').withHeader().parse(br);
) {
for(CSVRecord record : parser) {
userPojo = new UserPojo();
userPojo.setId(Integer.parseInt(record.get(CommonConstants.ID_COLUMN)));
userPojo.setName(record.get(CommonConstants.NAME_COLUMN));
userPojo.setDob(record.get(CommonConstants.DOB_COLUMN));
userPojo.setSalary(Double.parseDouble(record.get(CommonConstants.SALARY_COLUMN)));
break;
}
}catch (FileNotFoundException e) {
e.printStackTrace();
throw new ResourceNotFoundException("Resource not found.", Map.of("id", id, "message", "Resource Not Found"));
}
catch (Exception e) {
e.printStackTrace();
throw new FileParsingException("Error occurred while parsing the file.", Map.of("file", fileName, "message", "Error occurred while parsing the file.", "exception", e.toString()));
}
return userPojo;

}

public UserPojo parseXMLFile(String id, String fileName){
File xmlFile = new File(fileName);
UserPojo userPojo = null;
JAXBContext jaxbContext;
try
{
jaxbContext = JAXBContext.newInstance(UserPojo.class);

Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();

userPojo = (UserPojo) jaxbUnmarshaller.unmarshal(xmlFile);

}
catch (JAXBException e)
{
e.printStackTrace();
throw new FileParsingException("Error occurred while parsing the file.", Map.of("file", fileName, "message", "Error occurred while parsing the file.", "exception", e.toString()));
}

return userPojo;


}
}

/src/main/java/com/microservices/grpc/util/CommonUtility.java

package com.microservices.grpc.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson;
import com.microservices.grpc.CreateOrSaveUserResponse;
import com.microservices.grpc.User;
import com.microservices.grpc.pojo.Response;
import com.microservices.grpc.pojo.UserPojo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CommonUtility {

@Autowired
public Gson customGsonBuilder;
public User convertToProtoBuf(UserPojo userPojo) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(userPojo);
return customGsonBuilder.fromJson(jsonString, User.class);
}

public UserPojo convertToPojo(User user) throws IOException {
ObjectMapper mapper = new ObjectMapper();
String jsonString = customGsonBuilder.toJson(user);
return mapper.readValue(jsonString, UserPojo.class);
}

public Response getResponse(CreateOrSaveUserResponse createOrSaveUserResponse){
Response response = new Response();
response.setId(createOrSaveUserResponse.getId());
response.setStatusMessage(createOrSaveUserResponse.getStatusMessage());
response.setPath(createOrSaveUserResponse.getPath());
return response;
}

}

Sending the response back to the client

We make use of the util classes to perform the CRUD operations and get the final response in the protobuf format. Now the question is how do we send the response back to the client service? This is done using StreamObservers

Service implementations and clients use StreamObservers with onNext(), onError(), and onCompleted() methods to receive and publish messages using the gRPC framework.

Consider, get(GetUserRequest request, StreamObserver<User> responseObserver) in our case. We call responseObserver.onNext() method and pass the final User protobuf object to publish the User message back to the client. Once done, we call responseObserver.onComplemeted() which signifies the stream about the successful completion of the request.

Spring Boot Application Class

This is needed to start the backend server and accept RPC calls.

/src/main/java/GrpcServerApplication.java

package com.microservices.grpc;

import com.microservices.grpc.config.FileStorageProperties;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;

@SpringBootApplication
@EnableConfigurationProperties({
FileStorageProperties.class
})
public class GrpcServerApplication {
public static void main(String[] args){
SpringApplication.run(GrpcServerApplication.class, args);
}

}

The application class will detect the gRPC service and start the gRPC server on port 9000 by default. We can change the gRPC server port by changing the values in the application.yaml

/src/main/resources/application.yaml

grpc:
server:
port: 9000

Since the application needs to store the user details in CSV/XML files somewhere, we need to provide the storageDir path. That can be done by defining the storage dir path in application.yaml

file:
storageDir: <path_to_your_storage_directory>
grpc:
server:
port: 9000

NOTE: Ensure you replace the <path_to_your_storage_directory> in the application.yaml with a directory of your choice on your local machine.

How do you consume the storageDir property in our application?
By defining a configuration property class called FileStorageProperties.java . Annotate FileStorageProperties.java with @ConfigurationProperties. Register this class as a configuration property in GrpcServerApplication.java using @EnableConfigurationProperties.

/src/main/java/com/microservices/grpc/config/FileStorageProperties.java

package com.microservices.grpc.config;


import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "file")
public class FileStorageProperties {
private String storageDir;

public String getStorageDir() {
return storageDir;
}

public void setStorageDir(String storageDir) {
this.storageDir = storageDir;
}
}
@EnableConfigurationProperties
Usage of FileStorageProperties in FileUtility

Time to Test

Now we have the code ready to be tested. Let's get rolling!!

Run the server and client projects

Open the GrpcClientApplication.java and run the main method.

GrpcClientApplication.java

GrpcClientApplication will start on port 8080

GrpcClientApplication run console

Similarly, open the GrpcServerApplication and run the main method

GrpcServerApplication.java

GrpcServerApplication will start on port 9000

GrpcServerApplication run console

Ensure you have the following messages on the console after starting GrpcServerApplication

Open Postman

GET

  1. Select the GET request in POSTMAN
  2. Type the URL as http://localhost:8080/grpc/users/1234
  3. 1234 is the user which we still did not create. We will be creating it in POST request next.
  4. Submit the request. The response should return an error saying Resource Not Found as shown below.
GET rquest

POST

Let's create the user with id 1234.

{
"id": 1234,
"name": "Dev User",
"dob": "2023-05-17",
"salary": 100000.0
}
POST request
  • Provide the Request Header (fileType: CSV)as shown below
Request Header
  • Submit the request. You should get the response as 201 created
  • You can verify the creation of the user by submitting the GET request again. You should get the details now
GET: 1234
  • You can also find a CSV file created in your storage dir which you have given in the application.yaml . The CSV file name would be of the format <id>.<extension> i.e 1234.csv.
1234.csv
CSV file content

PUT

Let’s try to edit user 1234.

Change the name from “Dev User” to “DevName” and dob from “2023–05–17” to “2023–04-16”

{
"id": 1234,
"name": "Dev User",
"dob": "2023-05-17",
"salary": 100000.0
}
PUT request
  • Submit the request. You should get the success response with status message “User updated successfully”
Successful PUT request
  • Verify the changes by submitting the GET request again.
Updated user details
  • You can also find the CSV file updated in the storageDir.
1234.csv

Error Scenarios

The input validations are also in place. You can try it out by giving some invalid input values as shown below. The service will return appropriate error messages.

  • User already exists
  • Invalid name and salary fields
  • Invalid id in GET

Summary

In this article, we learned how to set up and run a basic client-server microservice application using gRPC protocol. Hope this blog helps anyone who is just getting started with gRPC and wants to get their hands dirty.

You can check out the full code here:

Feel free to provide your comments or suggestions. I would highly appreciate it. That’s all for today folks !!!

Happy Learning!

Story time: I mentioned about an interesting story above, which led to me writing this blog. Here it is! When I exploring opportunities back in 2021, one of the companies (will choose not to disclose the name) sent me mail to complete a small assignment to proceed with the recruitment process. The document had the same requirements what we discussed in this blog (There were some additional requirements as well which I skipped here to keep the scope short and simple). Back then, I did not know what gRPC was. Recently, when I was reading about Microservice communications, I bumped into gRPC again. I started to dig deeper. The more I read about it, the more interesting it seemed. That is when I decided to try my hands on this amazing and powerful framework and share my experience through this blog. I had a wonderful learning experience writing about it. Hope you will have one too after reading this blog!!

If you like to get more updates from me, please follow me on Medium and subscribe to email alert.

--

--