Kotlin gRPC with Spring πŸ‘‹βœ¨πŸ’«

Alexander Bryksin
9 min readNov 15, 2022

πŸ‘¨β€πŸ’» Full list of what has been used:

Spring web framework
Spring WebFlux Reactive REST Services
gRPC Kotlin gRPC
gRPC-Spring-Boot-Starter gRPC Spring Boot Starter
Spring Data R2DBC is a specification to integrate SQL databases using reactive drivers
Zipkin open source, end-to-end distributed tracing
Spring Cloud Sleuth autoconfiguration for distributed tracing
Prometheus monitoring and alerting
Grafana for to compose observability dashboards with everything from Prometheus
Kubernetes automates the deployment, scaling, and management of containerized applications
Docker and docker-compose
Helm The package manager for Kubernetes
Flywaydb for migrations

Source code you can find in the GitHub repository. For this project let’s implement Kotlin gRPC microservice using Spring and Postgresql. gRPC is very good for low latency and high throughput communication, so it’s great for microservices where efficiency is critical. Messages are encoded with Protobuf by default. While Protobuf is efficient to send and receive its binary format. Spring doesn’t provide us gRPC starter out of the box, and we have to use a community one, the most popular are yidongnan and LogNet, both are good and ready to use, for this project selected the first one. As the first step, we have to add gRPC Kotlin Codegen Plugin for Protobuf Compiler.

All UI interfaces will be available on ports:

Swagger UI: http://localhost:8000/webjars/swagger-ui/index.html

Grafana UI: http://localhost:3000

Zipkin UI: http://localhost:9411

Prometheus UI: http://localhost:9090

Docker-compose file for this project:

version: "3.9"

services:
microservices_postgresql:
image: postgres:latest
container_name: microservices_postgresql
expose:
- "5432"
ports:
- "5432:5432"
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=bank_accounts
- POSTGRES_HOST=5432
command: -p 5432
volumes:
- ./docker_data/microservices_pgdata:/var/lib/postgresql/data
networks: [ "microservices" ]

prometheus:
image: prom/prometheus:latest
container_name: prometheus
ports:
- "9090:9090"
command:
- --config.file=/etc/prometheus/prometheus.yml
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
networks: [ "microservices" ]

node_exporter:
container_name: microservices_node_exporter
restart: always
image: prom/node-exporter
ports:
- '9101:9100'
networks: [ "microservices" ]

grafana:
container_name: microservices_grafana
restart: always
image: grafana/grafana
ports:
- '3000:3000'
networks: [ "microservices" ]

zipkin:
image: openzipkin/zipkin:latest
restart: always
container_name: microservices_zipkin
ports:
- "9411:9411"
networks: [ "microservices" ]

networks:
microservices:
name: microservices

gRPC messages are serialized using Protobuf, an efficient binary message format, it serializes very quickly on the server and client, and its serialization results in small message payloads, important in limited bandwidth scenarios like mobile apps. The interface contract for specifying the RPC definitions for each service would be defined using Protocol Buffers. Each microservice will have a proto file defined here for this. At the first we have to define a service in a proto file and compile it, it has at most unary methods and one server streaming:

syntax = "proto3";

package com.example.grpc.bank.service;

import "google/protobuf/wrappers.proto";
import "google/protobuf/timestamp.proto";

service BankAccountService {
rpc createBankAccount (CreateBankAccountRequest) returns (CreateBankAccountResponse);
rpc getBankAccountById (GetBankAccountByIdRequest) returns (GetBankAccountByIdResponse);
rpc depositBalance (DepositBalanceRequest) returns (DepositBalanceResponse);
rpc withdrawBalance (WithdrawBalanceRequest) returns (WithdrawBalanceResponse);
rpc getAllByBalance (GetAllByBalanceRequest) returns (stream GetAllByBalanceResponse);
rpc getAllByBalanceWithPagination(GetAllByBalanceWithPaginationRequest) returns (GetAllByBalanceWithPaginationResponse);
}

message BankAccountData {
string id = 1;
string firstName = 2;
string lastName = 3;
string email = 4;
string address = 5;
string currency = 6;
string phone = 7;
double balance = 8;
string createdAt = 9;
string updatedAt = 10;
}

message CreateBankAccountRequest {
string email = 1;
string firstName = 2;
string lastName = 3;
string address = 4;
string currency = 5;
string phone = 6;
double balance = 7;
}

message CreateBankAccountResponse {
BankAccountData bankAccount = 1;
}

message GetBankAccountByIdRequest {
string id = 1;
}

message GetBankAccountByIdResponse {
BankAccountData bankAccount = 1;
}

message DepositBalanceRequest {
string id = 1;
double balance = 2;
}

message DepositBalanceResponse {
BankAccountData bankAccount = 1;
}

message WithdrawBalanceRequest {
string id = 1;
double balance = 2;
}

message WithdrawBalanceResponse {
BankAccountData bankAccount = 1;
}

message GetAllByBalanceRequest {
double min = 1;
double max = 2;
int32 page = 3;
int32 size = 4;
}

message GetAllByBalanceResponse {
BankAccountData bankAccount = 1;
}

message GetAllByBalanceWithPaginationRequest {
double min = 1;
double max = 2;
int32 page = 3;
int32 size = 4;
}

message GetAllByBalanceWithPaginationResponse {
repeated BankAccountData bankAccount = 1;
int32 page = 2;
int32 size = 3;
int32 totalElements = 4;
int32 totalPages = 5;
bool isFirst = 6;
bool isLast = 7;
}

The actual maven dependencies for gRPC:

<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-kotlin-stub</artifactId>
<version>${grpc.kotlin.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${java.grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-kotlin</artifactId>
<version>${protobuf.version}</version>
</dependency>
</dependencies>

And the maven protobuf plugin:

<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<executions>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.protoc.version}:exe:${os.detected.classifier}
</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${java.grpc.version}:exe:${os.detected.classifier}
</pluginArtifact>
<protocPlugins>
<protocPlugin>
<id>grpc-kotlin</id>
<groupId>io.grpc</groupId>
<artifactId>protoc-gen-grpc-kotlin</artifactId>
<version>${grpc.kotlin.version}</version>
<classifier>jdk8</classifier>
<mainClass>io.grpc.kotlin.generator.GeneratorRunner</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
</execution>
<execution>
<id>compile-kt</id>
<goals>
<goal>compile-custom</goal>
</goals>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.protoc.version}:exe:${os.detected.classifier}
</protocArtifact>
<outputDirectory>${project.build.directory}/generated-sources/protobuf/kotlin</outputDirectory>
<pluginId>kotlin</pluginId>
</configuration>
</execution>
</executions>
</plugin>

The plugin generates a class for each of your gRPC services. For example, BankAccountGrpcServiceGrpc where BankAccountGrpcService is the name of the gRPC service in the proto file. This class contains both the client stubs and the server ImplBase that you will need to extend. After compilation is done, we can implement our gRPC service. @GrpcService allows us to pass the list of interceptors specific to this service, so we can add LogGrpcInterceptor here. For request validation let’s use spring-boot-starter-validation which uses Hibernate Validator

Interceptors are a gRPC concept that allows apps to interact with incoming or outgoing calls. They offer a way to enrich the request processing pipeline. We can add interceptors, here we implement LogGrpcInterceptor and add it to the global GrpcGlobalServerInterceptor:

and add it to the global GrpcGlobalServerInterceptor:

The service layer of the microservice has a few methods, for example, working with lists of data it has two methods, one which returns Page used in unary method response and one returns Flow for gRPC streaming response method. The current Spring version supports @Transactional annotation with R2DBC The interface and implementation are below:

R2DBC is an API that provides reactive, non-blocking APIs for relational databases. Using this, you can have your reactive APIs in Spring Boot read and write information to the database in a reactive/asynchronous way. The BankRepository is a combination of CoroutineSortingRepository from spring data and our custom BankPostgresRepository implementation. For our custom implementation used here R2dbcEntityTemplate and DatabaseClient. If we want to have the same pagination response as JPA provides, we have to manually create PageImpl.

Repository implementation:

For error handling gRPC starter provide us GrpcAdvice which marks a class to be checked up for exception handling methods, @GrpcExceptionHandler marks the annotated method to be executed, in case of the specified exception is being thrown, status codes are well described here

For working with gRPC available few UI clients, personally like to use BloomRPC, other useful tools are grpcurl and grpcui.

Next step let’s deploy our microservice to k8s, we can build a docker image in different ways, in this example using a simple multistage docker file:

FROM --platform=linux/arm64 azul/zulu-openjdk-alpine:17 as builder
ARG JAR_FILE=target/KotlinSpringGrpc-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM azul/zulu-openjdk-alpine:17
COPY --from=builder dependencies/ ./
COPY --from=builder snapshot-dependencies/ ./
COPY --from=builder spring-boot-loader/ ./
COPY --from=builder application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher", "-XX:MaxRAMPercentage=75", "-XX:+UseG1GC"]

For working with k8s like to use Helm, deployment for the microservice is simple and has deployment itself, Service, ConfigMap, and ServiceMonitor. The last one is required because for monitoring use kube-prometheus-stack helm chart

Microservice helm chart yaml file is:

apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.microservice.name }}
labels:
app: {{ .Values.microservice.name }}
spec:
replicas: {{ .Values.microservice.replicas }}
template:
metadata:
name: {{ .Values.microservice.name }}
labels:
app: {{ .Values.microservice.name }}
spec:
containers:
- name: {{ .Values.microservice.name }}
image: {{ .Values.microservice.image }}
imagePullPolicy: Always
resources:
requests:
memory: {{ .Values.microservice.resources.requests.memory }}
cpu: {{ .Values.microservice.resources.requests.cpu }}
limits:
memory: {{ .Values.microservice.resources.limits.memory }}
cpu: {{ .Values.microservice.resources.limits.cpu }}
livenessProbe:
httpGet:
port: {{ .Values.microservice.livenessProbe.httpGet.port }}
path: {{ .Values.microservice.livenessProbe.httpGet.path }}
initialDelaySeconds: {{ .Values.microservice.livenessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.microservice.livenessProbe.periodSeconds }}
readinessProbe:
httpGet:
port: {{ .Values.microservice.readinessProbe.httpGet.port }}
path: {{ .Values.microservice.readinessProbe.httpGet.path }}
initialDelaySeconds: {{ .Values.microservice.readinessProbe.initialDelaySeconds }}
periodSeconds: {{ .Values.microservice.readinessProbe.periodSeconds }}
ports:
- containerPort: {{ .Values.microservice.ports.http.containerPort }}
name: {{ .Values.microservice.ports.http.name }}
- containerPort: {{ .Values.microservice.ports.grpc.containerPort}}
name: {{ .Values.microservice.ports.grpc.name }}
env:
- name: SPRING_APPLICATION_NAME
value: microservice_k8s
- name: JAVA_OPTS
value: "-XX:+UseG1GC -XX:MaxRAMPercentage=75"
- name: SERVER_PORT
valueFrom:
configMapKeyRef:
key: server_port
name: {{ .Values.microservice.name }}-config-map
- name: GRPC_SERVER_PORT
valueFrom:
configMapKeyRef:
key: grpc_server_port
name: {{ .Values.microservice.name }}-config-map
- name: SPRING_ZIPKIN_BASE_URL
valueFrom:
configMapKeyRef:
key: zipkin_base_url
name: {{ .Values.microservice.name }}-config-map
- name: SPRING_R2DBC_URL
valueFrom:
configMapKeyRef:
key: r2dbc_url
name: {{ .Values.microservice.name }}-config-map
- name: SPRING_FLYWAY_URL
valueFrom:
configMapKeyRef:
key: flyway_url
name: {{ .Values.microservice.name }}-config-map
restartPolicy: Always
terminationGracePeriodSeconds: {{ .Values.microservice.terminationGracePeriodSeconds }}
selector:
matchLabels:
app: {{ .Values.microservice.name }}

---

apiVersion: v1
kind: Service
metadata:
name: {{ .Values.microservice.name }}-service
labels:
app: {{ .Values.microservice.name }}
spec:
selector:
app: {{ .Values.microservice.name }}
ports:
- port: {{ .Values.microservice.service.httpPort }}
name: http
protocol: TCP
targetPort: http
- port: {{ .Values.microservice.service.grpcPort }}
name: grpc
protocol: TCP
targetPort: grpc
type: ClusterIP

---

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
release: monitoring
name: {{ .Values.microservice.name }}-service-monitor
namespace: default
spec:
selector:
matchLabels:
app: {{ .Values.microservice.name }}
endpoints:
- interval: 10s
port: http
path: /actuator/prometheus
namespaceSelector:
matchNames:
- default

---

apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.microservice.name }}-config-map
data:
server_port: "8080"
grpc_server_port: "8000"
zipkin_base_url: zipkin:9411
r2dbc_url: "r2dbc:postgresql://postgres:5432/bank_accounts"
flyway_url: "jdbc:postgresql://postgres:5432/bank_accounts"

and Values.yaml file:

microservice:
name: kotlin-spring-microservice
image: alexanderbryksin/kotlin_spring_grpc_microservice:latest
replicas: 1
livenessProbe:
httpGet:
port: 8080
path: /actuator/health/liveness
initialDelaySeconds: 60
periodSeconds: 5
readinessProbe:
httpGet:
port: 8080
path: /actuator/health/readiness
initialDelaySeconds: 60
periodSeconds: 5
ports:
http:
name: http
containerPort: 8080
grpc:
name: grpc
containerPort: 8000
terminationGracePeriodSeconds: 20
service:
httpPort: 8080
grpcPort: 8000
resources:
requests:
memory: '6000Mi'
cpu: "3000m"
limits:
memory: '6000Mi'
cpu: "3000m"

As a UI tool for working with k8s, personally like to use Lens.

More details and source code of the full project can find the GitHub repository here, of course always in real-world projects, business logic and infrastructure code are much more complicated, and we have to implement many more necessary features. I hope this article is useful and helpful, and be happy to receive any feedback or questions, feel free to contact me by email or any messengers :)

--

--