gRPC (Google Remote Procedure Calls):
is an open-source remote procedure call (RPC) system originally developed at Google in 2015. It uses HTTP/2 as its transport and Protocol Buffers as its interface description language. gRPC provides features such as authentication, bidirectional streaming and flow control, blocking or non-blocking bindings, and cancellation and timeouts. It generates cross-platform client and server bindings for many languages. It is most commonly used to connect services in a microservices-style architecture and to connect mobile devices and browser clients to back-end services.
Protocol Buffers:
a protocol for serialization (transfer) of structured data, proposed by Google as an effective binary alternative to the text format XML. The developers report that Protocol Buffers are simpler, more compact and faster than XML, since they transmit binary data optimized for a minimum message size.
Why gRPC is better than REST for internal services:
HTTP/2 binary format (protobuf) → less traffic and CPU.
Multiplexing in one TCP connection → fewer connections, lower latency.
One-way, server-side and two-way streams without long-polling.
Generated type-safe clients in 15+ languages.
Contract-first: schema changes are caught at compile time.
Built-in deadline, retry, load-balancing, TLS.
Observability: Metadata (headers/trailers) is suitable for tracing and metrics.
Easy to port externally: you can put grpc-gateway in front of the service and get REST/JSON without rewriting the code.
For example, let's make grpc-server and grpc-client
Project structure
simple-grpc │ settings.gradle.kts ├─grpc-server │ ├─build.gradle.kts │ ├─src/main/proto/hello.proto │ └─src/main/kotlin/com/example/grpc/Server.kt └─grpc-client ├─build.gradle.kts ├─src/main/proto/hello.proto └─src/main/kotlin/com/example/grpc/Client.kt
The *.proto file contains information about which messages and methods the service understands.
Roughly like a contract between a client and a server:
service:
lists the available operations (RPC methods).
message:
describes the request and response fields (type, name, ordinal number).
The file version is in the VCS; both sides generate code from this single schema and get strictly type-safe classes.
Why is it needed:
Single source of truth – neither the client nor the server can be "out of sync" in format.
Autogeneration – you only need to write logic, not serialization/deserialization.
Checking at the assembly stage – if you change a field in the contract, the compiler will immediately show where in the code you need to correct it.
Multilingualism – from the same proto you can get a client in Kotlin, Go, Python, etc. in a couple of commands.
*.proto describes the API once; everything else is generated.
After generation, GreeterGrpcKt.GreeterCoroutineImplBase (for the server) and GreeterCoroutineStub (for the client) will appear.
src/main/proto/hello.proto (copy to both modules)
syntax = "proto3"; package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
grpc-server/build.gradle.kts
plugins { kotlin("jvm") version "2.2.0" id("application") id("com.google.protobuf") version "0.9.4" } repositories { mavenCentral() } dependencies { implementation("io.grpc:grpc-kotlin-stub:1.4.3") implementation("io.grpc:grpc-protobuf:1.73.0") implementation("io.grpc:grpc-stub:1.73.0") implementation("com.google.protobuf:protobuf-kotlin:4.31.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.21") runtimeOnly("io.grpc:grpc-netty-shaded:1.73.0") } application { mainClass.set("com.example.grpc.ServerKt") } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.31.1" } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.73.0" } id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.3:jdk8@jar" } } generateProtoTasks.all().forEach { t -> t.plugins { id("grpc"); id("grpckt") } } }
grpc-client/build.gradle.kts (only mainClass differs)
plugins { kotlin("jvm") version "2.2.0" id("application") id("com.google.protobuf") version "0.9.4" } repositories { mavenCentral() } dependencies { implementation("io.grpc:grpc-kotlin-stub:1.4.3") implementation("io.grpc:grpc-protobuf:1.73.0") implementation("io.grpc:grpc-stub:1.73.0") implementation("com.google.protobuf:protobuf-kotlin:4.31.1") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.21") runtimeOnly("io.grpc:grpc-netty-shaded:1.73.0") } application { mainClass.set("com.example.grpc.ClientKt") } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.31.1" } plugins { id("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:1.73.0" } id("grpckt") { artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.3:jdk8@jar" } } generateProtoTasks.all().forEach { t -> t.plugins { id("grpc"); id("grpckt") } } }
Server.kt
package com.example.grpc import greet.GreeterGrpcKt import greet.HelloReply import greet.HelloRequest import io.grpc.ServerBuilder class GreeterService : GreeterGrpcKt.GreeterCoroutineImplBase() { override suspend fun sayHello(request: HelloRequest): HelloReply = HelloReply.newBuilder() .setMessage("Hello, ${request.name}") .build() } fun main() { val server = ServerBuilder .forPort(9090) .addService(GreeterService()) .build() .start() println("gRPC server on 9090") Runtime.getRuntime().addShutdownHook(Thread { server.shutdown() }) server.awaitTermination() }
Client.kt
package com.example.grpc import greet.GreeterGrpcKt import greet.HelloRequest import io.grpc.ManagedChannelBuilder import kotlinx.coroutines.runBlocking fun main() = runBlocking { val channel = ManagedChannelBuilder .forAddress("localhost", 9090) .usePlaintext() .build() val stub = GreeterGrpcKt.GreeterCoroutineStub(channel) val reply = stub.sayHello( HelloRequest.newBuilder().setName("Roman").build() ) println(reply.message) channel.shutdownNow() }
After launching the client will output Hello, Roman
, confirming that the gRPC call worked.
Examples of generated classes:
HelloRequestKt.kt, HelloReplyKt.kt:
Kotlin versions of messages; each has a Builder, parseFrom, toByteArray methods.
GreeterGrpcKt.kt contains two classes:
GreeterCoroutineImplBase:
base abstract class from which the server inherits; sayHello needs to be implemented.
GreeterCoroutineStub:
type-safe client; sayHello method is a regular suspend.
GreeterGrpc.java (not shown):
same, but without coroutines; remains if Java code is needed.
HelloRequestKt.kt (protobuf-kotlin)
// AUTO-GENERATED BY PROTOC, DO NOT EDIT package greet import com.google.protobuf.kotlin.* @OptIn(ProtobufSyntaxSupport::class) class HelloRequest private constructor( _name: String = "" ) : com.google.protobuf.GeneratedMessageLite< HelloRequest, HelloRequest.Builder>(DEFAULT_INSTANCE) { var name: String = _name private set // builder pattern class Builder : com.google.protobuf.GeneratedMessageLite.Builder< HelloRequest, Builder>(DEFAULT_INSTANCE) { fun setName(value: String): Builder = apply { instance.name = value } fun build(): HelloRequest = instance } companion object { private val DEFAULT_INSTANCE = HelloRequest() fun newBuilder(): Builder = Builder() @JvmStatic fun parseFrom(bytes: ByteArray) = DEFAULT_INSTANCE.parseFrom(bytes) } }
HelloReplyKt.kt
package greet import com.google.protobuf.kotlin.* class HelloReply private constructor( _message: String = "" ) : com.google.protobuf.GeneratedMessageLite< HelloReply, HelloReply.Builder>(DEFAULT_INSTANCE) { var message: String = _message private set class Builder : com.google.protobuf.GeneratedMessageLite.Builder< HelloReply, Builder>(DEFAULT_INSTANCE) { fun setMessage(v: String): Builder = apply { instance.message = v } fun build(): HelloReply = instance } companion object { private val DEFAULT_INSTANCE = HelloReply() fun newBuilder(): Builder = Builder() } }
GreeterGrpcKt.kt (gRPC-Kotlin plugin)
// AUTO-GENERATED BY PROTOC, DO NOT EDIT package greet import io.grpc.kotlin.* import kotlinx.coroutines.flow.Flow object GreeterGrpcKt { const val SERVICE_NAME: String = "greet.Greeter" abstract class GreeterCoroutineImplBase( coroutineContext: kotlin.coroutines.CoroutineContext = kotlinx.coroutines.Dispatchers.Default ) : AbstractCoroutineServerImpl(coroutineContext) { open suspend fun sayHello(request: HelloRequest): HelloReply = throw io.grpc.Status.UNIMPLEMENTED.asRuntimeException() final override fun bindService() = service { unaryRpc( method = METHOD_SAY_HELLO, implementation = ::sayHello ) } } class GreeterCoroutineStub private constructor( channel: io.grpc.Channel, callOptions: io.grpc.CallOptions ) : AbstractCoroutineStub<GreeterCoroutineStub>(channel, callOptions) { suspend fun sayHello(request: HelloRequest): HelloReply = unaryRpc( channel, METHOD_SAY_HELLO, request, callOptions, HelloReply.getDefaultInstance() ) override fun build(channel: io.grpc.Channel, callOptions: io.grpc.CallOptions) = GreeterCoroutineStub(channel, callOptions) } private val METHOD_SAY_HELLO = io.grpc.MethodDescriptor.newBuilder<HelloRequest, HelloReply>() .setType(io.grpc.MethodDescriptor.MethodType.UNARY) .setFullMethodName(io.grpc.MethodDescriptor.generateFullMethodName( SERVICE_NAME, "SayHello")) .setRequestMarshaller(protoLiteRequestMarshaller(HelloRequest.getDefaultInstance())) .setResponseMarshaller(protoLiteResponseMarshaller(HelloReply.getDefaultInstance())) .build() }