Patterns behind gRPC & Spring Boot gRPC Application
gRPC is an open-source framework developed by Google for building high-performance, scalable, and efficient distributed systems. gRPC is made based on the Remote Procedure Call. For more information RPC. Basically, RPC is a communication protocol that enables a program to execute code or procedures on another address space, typically on a remote server, as if they were local, facilitating distributed computing.
gRPC Provide
Synchronous Communication: Typically, the client sends a request to the server and waits for a response, making RPCs synchronous in nature.
Data Serialization: Data sent over the network needs to be serialized (converted into a stream of bytes) and then deserialized on the receiving end.
Why gRPC
Microservices Boost: gRPC is great for quick and efficient communication between microservices, ensuring low delays and high data flow.
Real-Time Chat: gRPC is awesome for chatting back and forth in real-time, letting services send messages instantly without repeatedly asking, like constant polling.
Language Variety: gRPC plays nice with many programming languages, making it a good pick when you’ve got different languages in your system.
Lighter Data: Thanks to gRPC using Protobuf, the messages sent are smaller compared to using JSON. This is handy when your network isn’t super fast.
Explanation patterns behind gRPC
1-) Unary RPC
The most basic pattern gRPC uses is the Unary RPC, which allows a client to send a single request to the server and receive a single response.
For example, there is a customer who would like to learn the price of the product then he sends the request to Server 2 which provides product information. This flow can be provided by Unary RPC. Because It is quite simple one request and one response.
service DiscountService {
rpc getDiscount(DiscountRequest) returns (DiscountResponse);
}
// exmaple definition
2-) Server Streaming RPC
Server Streaming RPC allows a client to send a request to a server and receive a bunch of streams of responses in return. This is useful for cases where the server needs to send a bunch of data to the client.
As a client, when you want the current weather by location, Service 2 can provide this information using Server Streaming RPC. This is because the weather information is provided by Server 2 in real-time according to this scenario.
You can provide the ‘stream’ keyword in the proto file.
service DiscountService {
rpc getDiscount(DiscountRequest) returns (stream DiscountResponse);
}
// example definition
3-) Client Streaming RPC
Client Streaming RPC allows a client to send a stream of requests to a server and receive a single response. This pattern is useful if the client needs to send a bunch of data to the server.
When the client would like to upload a file. This pattern will be entirely useful
service DiscountService {
rpc getDiscount(stream DiscountRequest) returns (DiscountResponse);
}
// example definition
4-) Bidirectional Streaming RPC
Bidirectional RPC allows a client to send a stream of requests to a server and receive a stream of responses. This is useful if the client needs to send to a server and receive a bunch of data from a server.
Bidirectional Streaming is useful in scenarios where there is a need for continuous communication and interaction between the clients and server. For example, a chat application where multiple users can send and receive messages in real-time.
service DiscountService {
rpc getDiscount(stream DiscountRequest) returns (stream DiscountResponse);
}
// example definition
Advantages of gRPC
1-) High Performance
gRPC uses HTTP/2 protocol for communication that provides multiplexing, flow control, and header compression. gRPC is a much more efficient communication between distributed systems than traditional REST API.
2-) Language Independence
gRPC provides multiple programming languages
3-) Strong Typing
gRPC uses Protocol Buffers for data serialization. This feature provides strong typing and reduces the risk of data errors.
Disadvantages of gRPC
1-) Complexity
gRPC can be more complex to set up and use than other RPC frameworks. Engineers need to understand the gRPC protocol and how to define service interfaces using PB (Protocol Buffers)
2-) Compatibility
gRPC may not be supported by the old systems
How Does gRPC Work
1-) Defining the service interface
2-) Generating code
3-) Client makes a request
4-) Serialization
5-) Network transport
6-) The server receives the request
7-) The server processes the request
8-) The server sends a response
9-) Network transport
10-) The client receives the response
11-) Client processes the response
How to Build gRPC Communication on Spring Boot
I created this application on my GitHub. I left the link in REFERENCES. Now I would like to share the schema of our distributed system. I can’t give all the details about each service because I do not want to write a book on Medium. If you have experience, you will understand when you look at the GitHub repo.
In this scenario, when the client wants to buy a book from our application library service gets a request from the client with a discount code. Library Service communicates with Discount Service if a discount code exists in the request which client sent. Discount Service consumes the request and runs business logic. For instance, check if a discount code exists in the PSQL database. If it exists, apply the discount percentage and return the result to the Library Service.
build.gradle
protobuf {
protoc { artifact = "com.google.protobuf:protoc:3.24.3" }
plugins {
grpc { artifact = "io.grpc:protoc-gen-grpc-java:1.58.0" }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}
sourceSets {
main {
java {
srcDirs 'build/generated/source/proto/main/grpc'
srcDirs 'build/generated/source/proto/main/java'
}
}
}
You can find the full file of build.gradle
This configuration, being generated files, is significantly important.
sourceSets configuration is to provide where files are generated by the compiler.
generateProtoTask sets up the generation of Proto tasks for all defined source sets. It specifies that the gRPC plugin should be applied for all Proto tasks.
Library Service
We must create a proto folder under src/main and create a Discount.proto files because gRPC communicates with proto buffers.
Discount.proto
syntax = "proto3";
option java_multiple_files = true;
package com.beratyesbek.grpc;
service DiscountService {
rpc getDiscount(DiscountRequest) returns (DiscountResponse);
}
message DiscountRequest {
string code = 1;
float price = 2;
int64 internalId = 3;
string bookName = 4;
}
message DiscountResponse {
string code = 1;
float newPrice = 2;
float oldPrice = 3;
Response response = 5;
}
message Response {
bool statusCode = 1;
string message = 2;
}
As you can see, we declared our services and models in a proto file. gRPC framework generates these models and services automatically. The numbers 1, 4,5, etc., associated with each field in the message definitions represent field numbers. These field numbers are used in the binary encoding. It enhances data integrity and provides faster mapping throughout serialization and deserialization.
giving a package name to proto files will be useful to determine which generated classes.
java_multiple_files provides to be generated of messages and services in individual files which is a cool feature for determining and debugging services.
DiscountService.java
package com.beratyesbek.libraryservice.grpc;
import com.beratyesbek.grpc.DiscountResponse;
import com.beratyesbek.libraryservice.entities.DbBook;
public interface DiscountGrpcService {
DiscountResponse getDiscount(DbBook dbBook, String code);
}
DiscountServiceImpl.java
package com.beratyesbek.libraryservice.grpc;
import com.beratyesbek.grpc.DiscountRequest;
import com.beratyesbek.grpc.DiscountResponse;
import com.beratyesbek.grpc.DiscountServiceGrpc;
import com.beratyesbek.libraryservice.entities.DbBook;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@Service
public class DiscountGrpcServiceImpl implements DiscountGrpcService {
private DiscountServiceGrpc.DiscountServiceBlockingStub discountServiceStub;
private ManagedChannel channel;
public DiscountGrpcServiceImpl(@Value("${discount.grpc.host}") String grpcHost, @Value("${discount.grpc.port}") int grpcPort) {
System.out.println("--> Discount Grpc information: " + grpcHost + ":" + grpcPort);
channel = ManagedChannelBuilder.forAddress(grpcHost, grpcPort)
.usePlaintext()
.build();
}
@Override
public DiscountResponse getDiscount(DbBook dbBook, String code) {
discountServiceStub = DiscountServiceGrpc.newBlockingStub(channel);
DiscountResponse discountResponse = discountServiceStub.getDiscount(
DiscountRequest.newBuilder()
.setCode(code)
.setBookName(dbBook.getName())
.setPrice(dbBook.getPrice().floatValue())
.setInternalId(dbBook.getId())
.build()
);
return discountResponse;
}
}
As you can see, we are creating a channel for communication with the Discount Service using grpcHost, grpcPort, and ManagedChannelBuilder on Constructor.
On the other hand, you do not have to create a channel there is a library that we already added to our gradle file that is to provide automatic channel creation. But I want to show both options.
You can access it from here
Discount Service
We must do the same thing for the Discount Service, such as the proto file. We should create a proto file under src/main/proto.
Discount.proto
syntax = "proto3";
option java_multiple_files = true;
package com.beratyesbek.grpc;
service DiscountService {
rpc getDiscount(DiscountRequest) returns (DiscountResponse);
}
message DiscountRequest {
string code = 1;
float price = 2;
int64 internalId = 3;
string bookName = 4;
}
message DiscountResponse {
string code = 1;
float newPrice = 2;
float oldPrice = 3;
Response response = 5;
}
message Response {
bool statusCode = 1;
string message = 2;
}
DiscountGrpcServiceImpl.java
package com.beratyesbek.discountservice.grpc;
import com.beratyesbek.discountservice.dao.DiscountDao;
import com.beratyesbek.discountservice.entities.DbDiscount;
import com.beratyesbek.grpc.DiscountRequest;
import com.beratyesbek.grpc.DiscountResponse;
import com.beratyesbek.grpc.DiscountServiceGrpc;
import com.beratyesbek.grpc.Response;
import io.grpc.stub.StreamObserver;
import lombok.AllArgsConstructor;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
@AllArgsConstructor
public class DiscountGrpcServiceImpl extends DiscountServiceGrpc.DiscountServiceImplBase {
private DiscountDao dao;
@Override
public void getDiscount(DiscountRequest request, StreamObserver<DiscountResponse> responseObserver) {
DbDiscount discount = dao.findDiscountByCode(request.getCode());
DiscountResponse discountResponse;
if (discount != null) {
discountResponse = DiscountResponse.newBuilder()
.setResponse(Response.newBuilder().setMessage("Code is valid").setStatusCode(true).build())
.setCode(request.getCode())
.setNewPrice(request.getPrice() - discount.getDiscountPrice())
.setOldPrice(request.getPrice())
.build();
} else {
discountResponse = DiscountResponse.newBuilder()
.setResponse(Response.newBuilder().setMessage("Code is invalid").setStatusCode(false).build())
.setCode(request.getCode())
.setNewPrice(request.getPrice())
.setOldPrice(request.getPrice())
.build();
}
responseObserver.onNext(discountResponse);
responseObserver.onCompleted();
}
}
This class is extended from DiscountServiceGrpc.DiscountServiceImplBase, which is generated automatically by the gRPC Framework. It behaviors as a Listener class. When a request comes from Library Service, the getDiscount() method triggers. Discount Service returns a response to the client and completes the request.
It essentially checks, if a given discount code is valid, retrieves the corresponding discount details, and responds with information including the validity status, discount code, new price, and old price.
@GrpcService annotation comes from the library that we already added to our build.gradle file. It indicates that this class is a gRPC Service in addition, it creates a channel using gRPC HOST and gRPC PORT by default configuration or our custom configuration at application.properties. If the annotation had not been declared on Discount Service, the Library Service would throw an exception like I could not connect this localhost and port.
onNext() method send the response back to the client
onCompleted() method Indicate we’re done with our response
Conclusion
It also depends on business requirements and the specific needs of your system. However, gRPC is generally faster than traditional REST APIs due to its architecture and protocol. According to my local tests, gRPC outperforms REST API services regarding communication speed and is also lighter.
On the flip side, implementing gRPC can be more challenging than traditional REST APIs. This difficulty is not necessarily because of the complexity of learning it, but rather due to developers being accustomed to certain practices. As humans, we tend to resist unfamiliar things. Particularly in large-scale systems handling numerous requests, adopting something unfamiliar might seem daunting.
In my opinion, I appreciate embracing challenges and adopting new technologies. Every technology comes with some advantages and disadvantages. I firmly believe that as developers, we must adapt ourselves to use new technologies. Furthermore, challenges are the best way to be an expert on something.
REFERENCES
NOTE: I quoted some places from these references