A Library to Make It Easier to Use Scala with gRPC
Posted on 02 May 2022, tagged Programming
Scala
gRPC
backend
This article describes why I created the library Scala2grpc.
gPRC is a Remote Procedure Call (RPC) framework made by Google. It uses a domain specific language (DSL) to define the APIs, and provides tools for lots of languages to generate code for both servers and clients. The generated code includes models and API interfaces. The developer can create a gRPC server by implementing the generated interfaces. There are lots of examples in the official document so I’ll not spend more time on the details.
It has lots of advantages compared to traditional HTTP APIs that encode payloads as JSON or XML. Just to name a few: it’s type safe so there are less places to make errors; it has a schema so causes less confusing when communicate APIs between developers; it’s more efficient on both serialization and translation. Because of the advantages and the big name behind it, it’s very popular, especially for mobile apps because of the good client support.
However, I feel the framework is very invasive: models are usually the foundations of a program. With gRPC, the models are generated by the framework, as well as the interfaces. This makes the whole program depends on the framework very heavily. Here is an example:
// ExampleService is generated by gRPC
class ExampleServiceImpl() extends ExampleService {
// ExampleInput and ExampleOutput are both generated by gRPC.
def exampleAPI(input ExampleInput): ExampleOut = {
...
}
}
It’s more invasive than most of the (non RPC) libraries: for most of other libraries, you can define the interface and use those libraries to fill in the implementations, so when you change a library you don’t need to change other parts of the code.
Maybe sometimes it doesn’t matter too much: say you are Google and this framework is so fundamental in the services that no one is gonna to change it. But if you don’t like it, a way to work around this is to define the business logic at another place, and invoke those native classes and methods in the implementation of the gRPC generated interfaces. The logic in these implementations should be as simple as possible, usually just the invoking of methods and the converting between gRPC models and native models. Here is an example of this approach:
// define natvie classes
case class MyInput(...)
case class MyOutput(...)
class MyService() {
def myAPI(input MyInput): MyOutput = {
...
}
}
// implement gRPC interfaces
// ExampleService is generated by gRPC
class ExampleServiceImpl(val myService: MyService) extends ExampleService {
// ExampleInput and ExampleOutput are both generated by gRPC.
def exampleAPI(input ExampleInput): ExampleOut = {
val myInput = convertFromGRPC(input)
val myOutput = myService.myAPI(myInput)
convertToGRPC(myOutput)
}
}
// implement convertFromGRPC ...
// implement convertToGRPC ...
However, this results lots of repeated works, especially for the converting between gRPC models and native models.
So here is where the sbt plugin Scala2grpc I’ve written comes in: it will generate all the proto files from the native Scala classes, and also generates the classes to convert between native models and gPRC models, and the classes to implement the gPRC interfaces. For example, in the example above, you only needs to write the code for MyInput
, MyOutput
and MyService
, the generation of the proto files are handled by Scala2grpc, as well as ExampleServiceImpl
, convertFromGRPC
and convertToGRPC
.
Sounds interesting? Check out the Github page to see how to use it. Don’t forget to star it if you find it helpful!