Skip to content

gRPC – How it works

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()
}

Copyright: Roman Kryvolapov