gRPC as Microservice Communication permalink
GRPC permalink
lets start this blog by asking some question about grpc
What is gRPC? permalink
Grpc is another HTTP protocol like REST, grpc stands for Google Remote Produceral Call. It Turn your code into calling a remote function using a contract called service, let's dive into the fundamental and why it's important.
What’s gRPC used to exchange data? permalink
In a REST World, we concatenate anything into a string using JSON, instead using JSON, grpc use Protocol Buffer, Protocol Buffer is just another form to exchange data between systems, lets save that in our brain, and ask more important question
Why gRPC? permalink
In a monolith architecture, it just functions call, right? wanna access the identity of a user? just function call straight into the db table and just find it, as simple as it is.
Well, in a microservices world, we have to call the other domain to access the data we need. On the first iteration of the product, we just use what is already working (REST) and flying data over the network to get the data we need and that is a problem. users don't really know and maybe dont wanna know what we are using to get their data, as long as it fast and stable, the users are happy.
Well, REST is not that fast, in almost all benchmark, we’ll see gRPC with protobuf beats REST with JSON, and question arises
Why We Need Faster Protocol than REST? permalink
think it's natural for us to have a question like that, and like all of our beloved seniors, the answer depends, if it's like our core service that need communications must be that fast, we have an option in our toolbelt now, its gRPC. When you implement your backend architecture with clean architecture, you can easily add gRPC without changing your code.
They use the same HTTP Protocol, but why gRPC faster? permalink
It's the protobuf, unlike JSON that uses string and just send it out, in a protobuf, the payload is encoded into binary, thats play important role on why gRPC is faster. now we have the big picture on why gRPC, lets dive-in into Protobuf
Protocol Buffer (Protobuf) permalink
Let's just focus on proto3 for now and see the big Picture
to define a Message Type, its
syntax = "proto3"
message AddUserRequest {
string username = 1;
string email = 2;
string password =3;
}
To Use proto3, we need add proto3 as a syntax and add name of object, in this case, is AddUserRequest, its object value type as an example, username has a datatype string and has a field number of 1, lets tackle that one by one, start by data type
Data Type permalink
you can read all the details in here, i just cover the big picture, every data types have a default values, if you don't set the values, it becomes to the default values, (if you are asking question like how to differentiate between default values and user set to zero values, we have the answer, just bear with me)
Numeric permalink
Default values is zero
floating number has double (float64) and float (float32)
whole number has int32,int64, uint32, uint64, and has exotic types like sint32, sint64, fixed64 etc that has specific purpose for efficiency, lets focus on regular type
String permalink
its just a string and default values is empty string
Bytes permalink
its just a bytes and default values is empty bytes
Bool permalink
just a bool true and false and default values is false
Enum permalink
you can define your own Enum's, and default values is the field number zero, so your Enum's must have zero field number, with example
enum DeliveryOrderStatus {
DELIVERY_ORDER_UNSPECIFIED = 0;
DELIVERY_ORDER_DRAFT = 1;
DELIVERY_ORDER_CONFIRMED = 2;
DELIVERY_ORDER_SCHEDULED =3;
}
Field Number permalink
Field number is an alias to a key in a protobuf,so it can encode the value efficiently.
Because the field number is a key in the protobuf implementation, it should be unique among of all its message and should never be reused to handle evolvability (backward compatibility and future compatibility).
If we want to extend the API contract, just use new field number, if we need to delete Fields and we want to maintain backward compatibility, we used reserved keyword, for example
syntax = "proto3"
message AddUserRequest {
reserved 1 to 3;
reserved "username","email","password";
string otp_token 4;
}
or just doesn’t bother with reserved keyword and create AddUserRequestV2
syntax = "proto3"
message AddUserRequestV2 {
string otp_token 1;
}
Field Labels permalink
If we don’t have label like the example above, it implicitly presence, so it always set to zero values when user/client doesn’t set the value, so we don't know when is user set zero value or user doesn't send the value, so, how we implement REST-PATCH solution in gRPC? we use optional, for example
message UpdateUserRequest {
optional string username = 1;
optional string email = 2;
optional string password = 3;
}
so, we can check if it's the value has been set by the user, in the implementation, it will compile to UpdateUserRequest.HasUsername() and return true if the user / client set the value for username
What about array? we use field labels repeated for that, for example
message PostTagResponse {
repeated string tags 1;
}
Message Type permalink
you can nest your message type, for example
message ListResponse {
message Metadata {
// your data types
}
message Data {
message PaginatedResult {
//your data type
}
repeated string ids 2;
}
}
I think that's it, there's another good feature in gRPC that is worth mentioning is, is datatype any and oneOf but it's will not be mentioned in here.
Let's Go to the next Step with Service, which creates explicit contract between your client and your server. Basically, it just an interface with Input and Output, for example, a whole user.proto file is like this
syntax="proto3";
package user;
message AddUserRequest {
string username = 1;
string email = 2;
string password =3;
}
message AddUserResponse {
string id = 1;
}
message GetUserByIdRequest {
string id = 1;
}
message GetUserByNameOrEmail {
oneof unique {
string username = 1;
string email = 2;
}
}
message GetUserResponse {
string username = 1;
string email = 2;
string hashed_password = 3;
}
service User {
rpc AddUser (AddUserRequest) returns (AddUserResponse);
rpc GetUserById (GetUserByIdRequest) returns (GetUserResponse);
rpc GetUser (GetUserByNameOrEmail) returns (GetUserResponse);
}
and .proto can be compiled to supported language by gRPC. For example, user.proto compiled and implemented in golang as a server
//...
type server struct {
usecase userSvc.Usecase
user.UnimplementedUserServer
}
func (s *server) AddUser(ctx context.Context, payload *user.AddUserRequest) (*user.AddUserResponse, error) {
resp, err := s.usecase.AddUser(ctx,
entites.User{
Username: payload.GetUsername(),
Email: payload.GetEmail(),
Password: payload.GetPassword(),
})
if err != nil {
return nil, handleError(ctx, err)
}
return &user.AddUserResponse{Id: resp}, nil
}
func (s *server) GetUser(ctx context.Context, payload *user.GetUserByNameOrEmail) (*user.GetUserResponse, error) {
resp, err := s.usecase.GetUser(ctx, entites.UserIdentifier{
Username: payload.GetUsername(),
Email: payload.GetEmail(),
})
if err != nil {
return nil, handleError(ctx, err)
}
return &user.GetUserResponse{
Username: resp.Username,
Email: resp.Email,
Hashedpassword: resp.Password,
}, nil
}
func (s *server) GetUserById(ctx context.Context, payload *user.GetUserByIdRequest) (*user.GetUserResponse, error) {
resp, err := s.usecase.GetUserById(ctx, payload.GetId())
if err != nil {
return nil, handleError(ctx, err)
}
return &user.GetUserResponse{
Username: resp.Username,
Email: resp.Email,
Hashedpassword: resp.Password,
}, nil
}
and as a client, you just Dial it, and save the connection, for example of client implementation in Go
func main() {
userClient, err := grpc.Dial(os.Getenv("USER_SERVICE"), grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("failed to dial user service client: %v", err)
}
defer userClient.Close()
userServiceClient := user.NewUserClient(userClient)
}
and we just called it like it's a function, for example in the BFF-REST application using Gin
type userHandler struct {
client user.UserClient
}
func (h *userHandler) PostUserHandler(ctx *gin.Context) {
var request struct {
Username string `json:"username" binding:"required,min=5,max=50"`
Email string `json:"email" binding:"required,email"`
PlainPassword string `json:"password" binding:"required,min=8"`
}
err := ctx.ShouldBindJSON(&request)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"errors": err.Error(),
})
return
}
// Call AddUser service
resp, err := h.client.AddUser(ctx, &user.AddUserRequest{
Username: request.Username,
Email: request.Email,
Password: request.PlainPassword,
})
if err != nil {
ctx.AbortWithStatus(http.StatusInternalServerError)
return
}
ctx.JSON(http.StatusCreated, gin.H{
"status": "success",
"data": gin.H{
"id": resp.GetId(),
},
})
}
So, if everything is self-managed and every microservice can use whatever languange they want, the contract should be in a github submodule, so a service can implement any. proto file and act as a “client” that calls to a “server” which is another service. for an example, you can implement it like this one below
now after seeing implementation details of an implementation in go, let's talk about lifecycle and gRPC itself
gRPC Lice Cycle permalink
gRPC has four life cycle, its Unary, Server Streaming, Client Streaming, and Bidirectional Streaming, its beyond of scope this doc to talk about four of those, let's talk about Unary and how similar it is to REST
Unary RPC permalink
- Client Calls
- Server Response
That’s It, basically like our beloved REST protocol, you can implement your own auth middleware in a gRPC, but first, you must be questioning like where do i put my auth token? in the request? do we have a header like REST to put our token and metadata? and the answer is yes, it's in the metadata, so you can have middleware to get token from Metadata, the example below is gRPC extract token from metadata implementation in Go
package auth
var (
headerAuthorize = "authorization"
)
func AuthFromMD(ctx context.Context, expectedScheme string) (string, error) {
val := metadata.ExtractIncoming(ctx).Get(headerAuthorize)
if val == "" {
return "", status.Error(codes.Unauthenticated, "Request unauthenticated with "+expectedScheme)
}
scheme, token, found := strings.Cut(val, " ")
if !found {
return "", status.Error(codes.Unauthenticated, "Bad authorization string")
}
if !strings.EqualFold(scheme, expectedScheme) {
return "", status.Error(codes.Unauthenticated, "Request unauthenticated with "+expectedScheme)
}
return token, nil
}
And Implementing An auth middleware, for example in Go
package middleware
import (
"context"
)
func AuthFn(func (ctx context.Context)) (context.Context, error) {
token, err := auth.AuthFromMD(ctx, "bearer")
if err != nil {
return nil, err
}
userId, err :=parseTokenWithSecret(token)
if err != nil {
return nil, err
}
ctx =context.WithValue(ctx, "user-id", userId)
return ctx, nil
}
and implementing middleware in when creating server in Go like example below
package main
import (
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/selector"
"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
)
func main() {
grpcSrv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
// Order matters e.g. tracing interceptor have to create span first for the later exemplars to work.
// Otel unused Midleware
//otelgrpc.UnaryServerInterceptor(),
//srvMetrics.UnaryServerInterceptor(grpcprom.WithExemplarFromContext(exemplarFromContext)),
//logging.UnaryServerInterceptor(interceptorLogger(rpcLogger), logging.WithFieldsFromContext(logTraceID)),
// authMiddleWare
selector.UnaryServerInterceptor(auth.UnaryServerInterceptor(middleware.AuthFn)),
// Recovery Middleware
recovery.UnaryServerInterceptor(recovery.WithRecoveryHandler(grpcPanicRecoveryHandler)),
),
)
}
and that's it about the happy path, you call sending the request, and you get the Expected Response, but what about unhappy path like error, how to handle it in gRPC?
in the gRPC Handler in a clean code architecture, you should handle Error that coming from dependency above, like usecase and repository. i like it to have global ErrorHandler like REST architecture. for example, using default errsdetail.BadRequest
package apperror
import (
"context"
"errors"
"sql"
"google.golang.org/grpc/status"
"google.golang.org/grpc/codes"
"spb google.golang.org/genproto/googleapis/rpc/status"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)
type AppError struct {
Err error
Message string
Detail map[string][]string
}
func (a AppError) Error() string {
return a.Err.Error()
}
func HandlegRPCError(ctx context.Context, err error) error {
if errors.Is(err, sql.ErrNoRows) {
return status.Error(codes.NotFound, "not found")
}
var appErr *AppError
if errors.As(err, &appErr) {
badRequest := &errdetails.BadRequest{}
for k, vs := range appErr.Detail {
for _, v := range vs {
badRequest.FieldViolations = append(
badRequest.FieldViolations,
&errdetails.BadRequest_FieldViolation{
Field: k,
Description: v,
})
}
}
st, err := status.New(codes.InvalidArgument, "invalid").WithDetails(badRequest)
if err != nil {
panic(fmt.Sprintf("Unexpected error attaching metadata: %v", err))
}
return st.Err()
}
span := trace.SpanFromContext(ctx)
span.RecordError(err)
log.Println(err)
return err
}
func HandlegRPCError(ctx context.Context, err error) error {
//your implementation on handling error in Http Protocol
}
you can implement your own error type too in protobuf and replace the errdetails with your own error type.
and after that, we can catch those error in the client like
st := status.Convert(err)
for _, detail := range st.Details() {
switch t := detail.(type) {
case *errdetails.BadRequest:
fmt.Println("Oops! Your request was rejected by the server.")
for _, violation := range t.GetFieldViolations() {
fmt.Printf("The %q field was wrong:\n", violation.GetField())
fmt.Printf("\t%s\n", violation.GetDescription())
}
}
}
i Think thats it, you can explore Protobuf in below:
gRPC in golang here:
https://grpc.io/docs/languages/go/quickstart/
useful middleware in golang here:
https://github.com/grpc-ecosystem/go-grpc-middleware
my unfinished repository about grpc-research as service communications