Building Microservices with Spring Boot and gRPC

A practical guide to gRPC Remote Procedure Call in Java

Jonathan Manera
7 min readAug 11, 2023
Images by Spring and gRPC

gRPC Remote Procedure Call is a binary message-based protocol for writing cross-language applications.

As part of this tutorial, we will see how the gRPC framework enables a more efficient communication across distributed systems.

Understanding gRPC

Client-Server Model

Similar to REST APIs, gRPC APIs define one or more services and request/response messages.

Client-Server model in gRPC

On the server-side, the gRPC server implements a service interface to handle client calls. While on the client-side, clients are provided with a gRPC stub that has the same methods as the server. This means you must first create your API (API-First approach).

The gRPC Protocol

The gRPC protocol is built on top of HTTP/2, which provides a foundation for long-lived, real-time communication streams.

The protocol introduces three concepts: channels, remote procedure calls (RPCs), and messages.

gRPC protocol

Each channel may have a number of RPCs, while each RPC may have a number of messages.

Client-Server Communication

Now we have a better understanding of how the gRPC protocol works, let’s examine four different ways clients and servers can communicate.

Client-Server communication in gRPC
  1. Unary RPC: It is the simplest type of RPC where the client sends a single request and gets back a single response.
  2. Server Streaming RPC: A server-streaming RPC is similar to a unary RPC, except that the server returns a stream of messages in response to a client’s request.
  3. Client Streaming RPC: A client-streaming RPC is similar to a unary RPC, except that the client sends a stream of messages to the server and the server responds with a single message.
  4. Bidirectional Streaming RPC: In a bidirectional streaming RPC, the call is initiated by the client invoking the method. The two streams are independent, so the client and server can read and write messages in any order.

Protocol Buffers

The Protocol Buffer language is a compact, language-neutral, platform-neutral extensible mechanism for serializing structured data. By default, gRPC uses the Protocol Buffers as both its Interface Definition Language (IDL) and as its underlying message interchange format.

The Protocol Buffer file is an ordinary text file with a .proto extension, which allows client-side stubs and service interface generation.

Let’s take the foo.proto file described below as an example.

syntax = "proto3";

package foo.grpc;

option java_multiple_files = true;

This example uses the proto3 syntax defined by the syntax property.

The compiler generates Java classes based on the .proto file. It is convenient to specify a destination package in the package property.

By default, the compiler generates a single Java file. In order to generate individual Java files, we set the java_multiple_files property to true.

Next, let’s define the FooRequest and FooResponse messages.

message FooRequest {
string bar = 1;
}

message FooResponse {
string result = 1;
}

Message fields must be defined with type, name and a number (usually auto-incremental).

To conclude, let’s define the service contract for FooService.

service FooService {

// Unary RPC
rpc foo(FooRequest) returns (FooResponse);

// Server Streaming RPC
rpc fooStreamResponses(FooRequest) returns (stream FooResponse);

// Client Streaming RPC
rpc fooStreamRequests(stream FooRequest) returns (FooResponse);

// Bidirectional Streaming RPC
rpc fooStreamRequestsAndResponses(stream FooRequest) returns (stream FooResponse);
}

Here, the foo() operation enables unary RPC, fooSreamResponses() allows server streaming, fooSreamRequests() allows client streaming, and fooSreamRequestsAndResponses() enables bidirectional streaming.

Note that by simply adding the stream keyword to the request/response, we can enable client/server streaming.

Spring Boot with gRPC

Scenario

The following example illustrates a simplified representation of a real-life distributed system.

Here, Loans Service is called by Accounts Service to create, read, update or delete loans.

Project Structure

As shown below, our Maven project is divided into three modules: common, loans and accounts.

├───accounts
│ ├───src
│ │ └───main
│ │ ├───java
│ │ └───resources
│ └───pom.xml
├───common
│ ├───src
│ │ └───main
│ │ └───proto
│ └───pom.xml
├────loans
│ ├───src
│ │ └───main
│ │ ├───java
│ │ └───resources
│ └───pom.xml
└───pom.xml

Creating a Common Module

The common module holds the .proto files and the Java code the compiler generates.

The first step is to define the loans.proto file in the common/src/main/proto directory.

syntax = "proto3";

import "google/protobuf/empty.proto";

package common.grpc.loans;

option java_multiple_files = true;

service LoansService {

rpc createLoan(Loan) returns (LoanId);

rpc readLoan(LoanId) returns (Loan);

rpc renegotiateLoan(Loan) returns (google.protobuf.Empty);

rpc deleteLoan(LoanId) returns (google.protobuf.Empty);
}

message Borrower {
string name = 1;
int32 age = 2;
double annual_income = 3;
optional double annual_debt = 4;
optional bool delinquent_debt = 5;
}

message Loan {
string guid = 1;
double requested_amount = 2;
int32 term_months = 3;
float annual_interest = 4;
Borrower borrower = 5;
}

message LoanId {
string guid = 1;
}

Next, in order to generate Java code using the protocol buffer compiler, we use the Maven Protocol Buffers plugin.

Let’s add the plugins and dependencies we need to common/pom.xml.

<properties>
<grpc.version>1.40.1</grpc.version>
<protobuf.version>3.19.6</protobuf.version>
</properties>

<dependencies>
<dependency>
<artifactId>grpc-netty-shaded</artifactId>
<groupId>io.grpc</groupId>
<scope>runtime</scope>
<version>${grpc.version}</version>
</dependency>
<dependency>
<artifactId>grpc-protobuf</artifactId>
<groupId>io.grpc</groupId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<artifactId>grpc-stub</artifactId>
<groupId>io.grpc</groupId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<artifactId>protobuf-java</artifactId>
<groupId>com.google.protobuf</groupId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<artifactId>javax.annotation-api</artifactId>
<groupId>javax.annotation</groupId>
<version>1.2</version>
</dependency>
</dependencies>

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

As a result, the plugin generates Java code in the target/classes directory each time Maven compiles the source code.

Creating the gRPC Server

The loans module has a simple implementation for the gRPC Server.

To use the Java code generated in the common module, we add a dependency to loans/pom.xml.

<dependency>
<artifactId>common</artifactId>
<groupId>com.manerajona</groupId>
<version>${project.version}</version>
</dependency>

Additionally, we want to add a dependency for the autoconfiguration of the embedded gRPC server. LogNet’s gRPC Spring Boot Starter provides us with this functionality.

<dependency>
<groupId>io.github.lognet</groupId>
<artifactId>grpc-spring-boot-starter</artifactId>
<version>5.1.4</version>
</dependency>

As a next step, we extend and override the default implementation of LoansServiceImplBase generated by the protocol buffer compiler.

@GRpcService
public class LoansServiceImpl extends LoansServiceImplBase {

private final ConcurrentMap<LoanId, Loan> loans = new ConcurrentHashMap<>();

@Override
public void createLoan(Loan loan, StreamObserver<LoanId> responseObserver) {
LoanId loanId = LoanId.newBuilder().setGuid(loan.getGuid()).build();
loans.put(loanId, loan);

responseObserver.onNext(loanId);
responseObserver.onCompleted();
}

@Override
public void readLoan(LoanId loanId, StreamObserver<Loan> responseObserver) {
Loan loan = loans.get(loanId);

responseObserver.onNext(loan);
responseObserver.onCompleted();
}

@Override
public void renegotiateLoan(Loan loan, StreamObserver<Empty> responseObserver) {
LoanId loanId = LoanId.newBuilder().setGuid(loan.getGuid()).build();
loans.computeIfPresent(loanId, (k, v) -> loan);

responseObserver.onNext(Empty.getDefaultInstance());
responseObserver.onCompleted();
}

@Override
public void deleteLoan(LoanId loanId, StreamObserver<Empty> responseObserver) {
loans.remove(loanId);

responseObserver.onNext(Empty.getDefaultInstance());
responseObserver.onCompleted();
}
}

Here, Loan objects are stored in ConcurrentMap<LoanId, Loan>.

A response is added after each operation using StreamObserver’s onNext() method, and the call is resumed after onCompleted().

In order to automatically configure the gRPC server, we annotate LoansServiceImpl with @GRpcService. By default the gRPC server will run on localhost:6565.

Creating the Client

The accounts module has a simple implementation the client.

First, we add the common and spring-boot-starter-web dependencies to accounts/pom.xml.

<dependency>
<artifactId>common</artifactId>
<groupId>com.manerajona</groupId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Next, let’s create a configuration class for the gRPC Service Stub.

@Configuration
class GrpcClientConfiguration {

@Bean
LoansServiceBlockingStub loansServiceStub() {
Channel channel = ManagedChannelBuilder.forAddress("localhost", 6565)
.usePlaintext()
.build();

return LoansServiceGrpc.newBlockingStub(channel);
}
}

Here, we have created a bean of type LoansServiceBlockingStub. This stub is blocking, meaning that for each request it blocks the RPC call until it receives a response. You can also create a non-blocking stub to make asynchronous calls.

Note that the LoansService.newBlockingStub() method accepts Channel as argument. By using ManagedChannelBuilder, we create the channel for the address and port where the server is running, and we specify usePlainText() to exchange messages without any encryption (not recommended.)

As a final step, we test the stub using Spring ApplicationRunner.

@Component
public class AccountsApplicationRunner implements ApplicationRunner {

private final LoansServiceBlockingStub stub;

public AccountsApplicationRunner(LoansServiceBlockingStub stub) {
this.stub = stub;
}

@Override
public void run(ApplicationArguments args) {

// create
var borrowerBuilder = Borrower.newBuilder()
.setName("John Doe")
.setAge(34)
.setAnnualIncome(100_000);

var loanBuilder = Loan.newBuilder()
.setGuid(UUID.randomUUID().toString())
.setBorrower(borrowerBuilder.build())
.setRequestedAmount(1_000_000_000)
.setTermMonths(12)
.setAnnualInterest(6.5f);

LoanId id = stub.createLoan(loanBuilder.build());
System.out.println("** Loan created with " + id);

// read
System.out.println("** Loan read:\n" + stub.readLoan(id));

// update
Loan loanUpdated = loanBuilder
.setTermMonths(24)
.setAnnualInterest(7.5f)
.build();

stub.renegotiateLoan(loanUpdated);
System.out.println("** Loan updated:\n" + stub.readLoan(id));

// delete
stub.deleteLoan(id);
System.out.println("** Is Loan deleted: " +
stub.readLoan(id).getGuid().isBlank());
}
}

This image shows each operation called and printed on the console:

Conclusion

In this article, we learned how to use gRPC in Java and Spring Boot. Let’s now look at some of the pros and cons of adopting this technology:

Pros:

  • It enables API-First design.
  • It enables streaming.
  • It is an efficient, compact and secure way to exchange messages between clients and servers.
  • It is agnostic to the programming language used in the application.

Cons:

  • There may be some challenges using it on the frontend.
  • Older infrastructure might not be ready for moving from HTTP/1 to HTTP/2.

Thanks for reading. I hope this was helpful!

The example code is available on GitHub.

--

--

Jonathan Manera

If you wish to make a Java app from scratch, you must first invent the universe.