What I Learned at Work this Week: gRPC and Java

Mike Diaz
5 min readJan 8, 2023
It all starts with a proto

I had my first experience with gRPC over a year ago, as I discussed in this post from August 2021. I’m sorry to say that I haven’t been working consistently with it since then, so I’ve had to re-learn a lot of the concepts. Further complicating things, our most common use case now is integrating gRPC calls into a Java API rather than using my preferred language of Python. Let this be a lesson to us all: if you pick something up, try to practice, or else you might forget it!

Quick Review

Let’s establish two things about gRPC off the bat:

  • It’s a tool for communicating between a client and a server.
  • It’s language agnostic, often using protocol buffers to serialize (standardize) data.

My Service

I was tasked with writing an endpoint that accepts a gRPC call from our frontend UI and processes it by adding a row to a DB. This row would trigger a script that processes unsubscribes or “opt outs” from my company’s product, so my business logic was contained in a class called OptOutConfigService. We can immediately see that this class is using gRPC:

@GRpcService
public class OptOutConfigService extends OptOutServiceGrpc.OptOutServiceImplBase {
private final OptOutConfigRepository optOutConfigRepository;

public OptOutConfigService(OptOutConfigRepository optOutConfigRepository) {
this.optOutConfigRepository = optOutConfigRepository;
}

Experienced Java programmers will recognize a lot of what’s going on here, but in the tradition of my blog, I’ll break it down. The very first line is a Spring Boot annotation. Spring Boot is a Java framework that abstracts a ton of connections and methods, making our code easier to use, but more difficult to explain. Even the tutorial I was following didn’t explain what exactly the annotation changed, but my service definitely didn’t work without it.

We got a bit more explanation for the next line, where our defined class, OptOutConfigService, extends OptOutServiceGrpc.OptOutServiceImplBase. The ImplBase class is defined in a generated file, in my case called OptOutServiceGrpc.java. According to yidongnan.github.io:

The configured maven/gradle protobuf plugins will then use invoke the protoc compiler with the protoc-gen-grpc-java plugin and generate the data classes, grpc service ImplBases and Stubs.

The ImplBase classes contain the base logic that map the dummy implementation to the grpc service methods. More about this in the Implementing the service topic.

This file was written by gradle through use of the protoc compiler, which bases its work of a very simple proto that I wrote (the screenshot at the top of this post). When we define our gRPC service class, we specifically refer to the ImplBase, which allows our service class to inherit the generated bindService method. When our service is built, bindService will attach the stubs which are critical to the communication between gRPC client and server. When I tried to test my API call with Postman, it couldn’t find the new method until I added the inheritance.

Business Logic

Now that we recognize the gRPC components of our service, we can see that the rest of the snippet is more basic Java. We instantiate an OptOutConfigRepository and then write a constructor that assigns it as a property of the class. The repository is where I can define the DB queries that I want my service to execute. The rest of the service contains “business logic,” meaning the code that determines when to run those queries and what to do if they fail. There aren’t many conditionals yet, so my logic is relatively simple:

public void addNewOptOutConfig(
OptOutConfigProtos.AddNewOptOutConfigRequest request,
StreamObserver<OptOutConfigProtos.AddNewOptOutConfigResponse> responseObserver) {
try {
OptOutConfig optOutConfig = new OptOutConfig(request);
optOutConfigRepository.insertNewOptOutConfig(optOutConfig).ifPresentOrElse(id -> {
responseObserver.onNext(OptOutConfigProtos.AddNewOptOutConfigResponse.newBuilder().
setMessage("Success! A new opt out row has been created").build());
responseObserver.onCompleted();
}, () -> {
throw new OptOutConfigException("Error during opt out insert");
});
} catch (SolutionsException e) {
responseObserver.onError(ErrorMessageUtil.sendErrorMessage(Code.INTERNAL, e.getMessage()));
}
}

The method addNewOptOutConfig accepts a request and a responseObserver argument. The request comes from the stub, meaning it’ll be in the shape I defined with my proto. The responseObserver is a stream of responses that are also defined by my proto. We format this as a stream so that we can be prepared if our request triggers multiple messages.

Wrapping our logic inside of a try, we instantiate an OptOutConfig by passing in our request. Remember that the request is shaped by an auto-generated class based off a proto I wrote. By passing it to a custom class which we define, we’ll have more control over its shape and methods like getters and setters.

We then execute a method from my repository, insertNewOptOutConfig. If you’re curious, here’s what that code looks like:

public Optional<Integer> insertNewOptOutConfig (OptOutConfig optOutConfig) {
try {
return jdbi.withHandle(handle -> handle.createUpdate(INSERT_OPT_OUT_CONFIG)
.bind("optOutType", "standard")
.bind("optOutIsActive", true)
.bind("companyId", Integer.parseInt(optOutConfig.getCompanyId()))
.executeAndReturnGeneratedKeys("id").mapTo(Integer.class).findFirst()
);
} catch (JdbiException | NoSuchElementException e) {
String message = "Error inserting opt out config.";
LOG.error(message, e);
throw new RepositoryException(message);
}
}

If you haven’t used jdbi before, just know that it’s a tool for making SQL queries with Java and .bind attaches variables to the query. One critical detail to recognize here is that insertNewOptOutConfig returns an Optional<Integer>. That is to say that it might return a value, it might not, but if there is a return, it should be an Integer.

In our service code, we chain a method called ifPresentOrElse after insertNewOptOutConfig. This method performs a specified Consumer action on the value of an Optional. It accepts two arguments:

  1. A Consumer action
  2. A Runnable emptyAction

If the Optional returns a value, the Consumer action will execute. In this case, that’s using our responseObserver to build a response for a successful run. onNext receives a value from the stream, in this case a built response with a success message. onCompleted assures that we end the stream. If the Optional returns void, ifPresentOrElse will trigger the second argument, an exception.

At the very end, we have a catch for the whole method. It’s helpful to utilize error or failure handling at different points so that we can better recognize what caused an issue. If I see the message “Error during opt out insert,” I’ll start by looking at the query. If I get the internal error code, I know it’s more likely that my service isn’t hooked up correctly.

Practice

It’s hard to get excited about the content of this post because I feel like I’ve covered it before. The first time I learned gRPC, I got a basic understanding that looks a lot like what I’ve done here, except in Python. There are some important takeaways, however. Like I said in the beginning, if we learn something new, we should try to stick with it or we risk having to re-learn it later. But also, if something doesn’t make total sense the first time, re-learning it may be beneficial because we’ll pick up some of the things we missed originally. I’m definitely more confident with gRPC than I was after I wrote my first post, plus I have no used it in both Java and Python, with some spring-boot thrown in to boot.

Sources:

--

--