Kotlin + gRPC: Build your first service in four steps
This is the first article in a hands-on series where we'll explore how to build gRPC services using Kotlin. The series is designed to help you understand not only the technical how-to, but also the design choices behind Protocol Buffers and gRPC. We'll start simple and gradually introduce more advanced concepts as we go. Here is what you can expect from the upcoming articles: In the next article, we will explore field presence, repeated, maps, enums, oneOf, and how to evolve your API safely. Then, we will expand our model to cover nesting, composition, validations, and idiomatic builder DSL. Later, we will explore gRPC streaming to build APIs that go beyond request/response, using Kotlin coroutines and flows. We'll also explore deadlines and error handling. Finally, as a bonus, we'll go over topics you should focus on if you need to implement gRPC for a production environment, like tooling, CI/CD and Architectural Practices. By the end of the series, you will have a solid understanding of gRPC in Kotlin, and a real working service you can build upon. If you've worked with REST APIs, you know they've become the standard way to build and expose web services. They're simple, flexible, and have broad tooling support. But when services need to communicate with each other efficiently, especially in a distributed or high-performance system, gRPC is often a better choice. In this first article, we will talk about what gRPC is, how it fits into a Kotlin ecosystem, and how to build a simple gRPC service and client from scratch. We will also compare it to REST and JSON, discuss its tradeoffs, and walk you through the tools and structure needed to get started. By the end, you'll understand how gRPC works, why it matters, and how to implement a basic service using Kotlin, Protocol Buffers, and gRPC. What is gRPC and why use it? gRPC is a framework created by Google for making remote procedure calls (RPC). It allows you to call methods on a service running on another machine as if they were local. Under the hood, it uses HTTP/2 and Protocol Buffers (Protobuf) for communication. Protocol Buffers are a language-neutral, platform-neutral mechanism for serializing structured data. You define your messages and services in a .proto file, and code is generated from that file in your target language. This allows for benefits such as: Strongly typed contracts between services. Code generation avoids boilerplate and reduces errors. Binary serialization for performance and smaller payloads. HTTP/2 enables multiplexing, streaming, and low latency. REST vs. gRPC: Tradeoffs It's important to understand that gRPC is not always "better" than REST. It's different. Choosing between REST and gRPC depends on your context, constraints, and goals. gRPC strengths: More efficient data transmission (Protobuf is binary, smaller than JSON). Strong typing and codegen reduce runtime errors. Supports streaming (one-way and bidirectional). Easier evolution with schema compatibility rules. REST strengths: Human-readable and debug-friendly (JSON over HTTP). Better support for web clients and public APIs. Mature ecosystem, including caching, proxies, and tools. Our Use Case: A simple Note service In this series, we will build a Note Service — something like a backend for a basic note-taking app. Over time, we will introduce complexity gradually. Here's what you'll see in future articles: Add support for tags, users, and note metadata (nested fields, repeated fields). Handle optional fields and versioning concerns as the schema evolves. Use streaming for features like syncing or watching updates in real-time. Learn about Protobuf features like oneof, field presence, and default values. Let's start small: a service to create a new note with a title and content. Project structure and tools To build our Kotlin gRPC service, we'll use: Kotlin/JVM as our programming language on the JVM platform. Gradle (Kotlin DSL) — dependency and build management. Protocol Buffers (.proto) — our API schema. protoc — the Protobuf compiler. gRPC Kotlin — gRPC support for Kotlin and coroutines. Our folder structure will start with something like this (a very basic Kotlin + Gradle project), but might evolve as we expand our project scope: note-service-kotlin-gprc/ ├── build.gradle.kts ├── settings.gradle.kts ├── src/ │ ├── main/ │ │ ├── kotlin/ # Kotlin code (server + client) │ │ └── proto/ # Protobuf files (.proto) The source code for the scope of each article will be made available in individual branches at the note-service-kotlin-gprc repository, while HEAD on the main branch will accommodate the latest working version. The specific branch for this article is article1-first-service. Step 1: Writing your first .proto file Let's define the initial structure of our Note Service using Protocol Buffers. This file describe

This is the first article in a hands-on series where we'll explore how to build gRPC services using Kotlin. The series is designed to help you understand not only the technical how-to, but also the design choices behind Protocol Buffers and gRPC. We'll start simple and gradually introduce more advanced concepts as we go.
Here is what you can expect from the upcoming articles:
- In the next article, we will explore field presence, repeated, maps, enums, oneOf, and how to evolve your API safely.
- Then, we will expand our model to cover nesting, composition, validations, and idiomatic builder DSL.
- Later, we will explore gRPC streaming to build APIs that go beyond request/response, using Kotlin coroutines and flows. We'll also explore deadlines and error handling.
- Finally, as a bonus, we'll go over topics you should focus on if you need to implement gRPC for a production environment, like tooling, CI/CD and Architectural Practices.
By the end of the series, you will have a solid understanding of gRPC in Kotlin, and a real working service you can build upon.
If you've worked with REST APIs, you know they've become the standard way to build and expose web services. They're simple, flexible, and have broad tooling support. But when services need to communicate with each other efficiently, especially in a distributed or high-performance system, gRPC is often a better choice.
In this first article, we will talk about what gRPC is, how it fits into a Kotlin ecosystem, and how to build a simple gRPC service and client from scratch. We will also compare it to REST and JSON, discuss its tradeoffs, and walk you through the tools and structure needed to get started.
By the end, you'll understand how gRPC works, why it matters, and how to implement a basic service using Kotlin, Protocol Buffers, and gRPC.
What is gRPC and why use it?
gRPC is a framework created by Google for making remote procedure calls (RPC). It allows you to call methods on a service running on another machine as if they were local. Under the hood, it uses HTTP/2 and Protocol Buffers (Protobuf) for communication.
Protocol Buffers are a language-neutral, platform-neutral mechanism for serializing structured data. You define your messages and services in a .proto
file, and code is generated from that file in your target language.
This allows for benefits such as:
- Strongly typed contracts between services.
- Code generation avoids boilerplate and reduces errors.
- Binary serialization for performance and smaller payloads.
- HTTP/2 enables multiplexing, streaming, and low latency.
REST vs. gRPC: Tradeoffs
It's important to understand that gRPC is not always "better" than REST. It's different. Choosing between REST and gRPC depends on your context, constraints, and goals.
gRPC strengths:
- More efficient data transmission (Protobuf is binary, smaller than JSON).
- Strong typing and codegen reduce runtime errors.
- Supports streaming (one-way and bidirectional).
- Easier evolution with schema compatibility rules.
REST strengths:
- Human-readable and debug-friendly (JSON over HTTP).
- Better support for web clients and public APIs.
- Mature ecosystem, including caching, proxies, and tools.
Our Use Case: A simple Note service
In this series, we will build a Note Service — something like a backend for a basic note-taking app. Over time, we will introduce complexity gradually.
Here's what you'll see in future articles:
- Add support for tags, users, and note metadata (nested fields, repeated fields).
- Handle optional fields and versioning concerns as the schema evolves.
- Use streaming for features like syncing or watching updates in real-time.
- Learn about Protobuf features like
oneof
, field presence, and default values.
Let's start small: a service to create a new note with a title and content.
Project structure and tools
To build our Kotlin gRPC service, we'll use:
- Kotlin/JVM as our programming language on the JVM platform.
- Gradle (Kotlin DSL) — dependency and build management.
-
Protocol Buffers (
.proto
) — our API schema. - protoc — the Protobuf compiler.
- gRPC Kotlin — gRPC support for Kotlin and coroutines.
Our folder structure will start with something like this (a very basic Kotlin + Gradle project), but might evolve as we expand our project scope:
note-service-kotlin-gprc/
├── build.gradle.kts
├── settings.gradle.kts
├── src/
│ ├── main/
│ │ ├── kotlin/ # Kotlin code (server + client)
│ │ └── proto/ # Protobuf files (.proto)
The source code for the scope of each article will be made available in individual branches at the note-service-kotlin-gprc repository, while HEAD
on the main
branch will accommodate the latest working version.
The specific branch for this article is article1-first-service.
Step 1: Writing your first .proto
file
Let's define the initial structure of our Note Service using Protocol Buffers. This file describes the types and service methods that gRPC will generate for us.
Create a file at src/main/proto/note_service.proto
with the following content:
syntax = "proto3";
package com.fugisawa.grpc.noteservice;
option java_package = "com.fugisawa.grpc.noteservice";
option java_multiple_files = true;
service NoteService {
rpc CreateNote (CreateNoteRequest) returns (Note);
}
message CreateNoteRequest {
string title = 1;
string content = 2;
}
message Note {
string id = 1;
string title = 2;
string content = 3;
}
Let's break this down:
syntax = "proto3";
This declares that we're using proto3, the most recent version of 'the Protocol Buffers language. It simplifies the syntax and sets some defaults (like making fields optional).
package note;
This is the Protobuf package name. It groups related messages and services under a common namespace to avoid naming conflicts.
option java_package
and java_multiple_files
These are used by the code generator for JVM languages (like Kotlin or Java):
java_package = "com.fugisawa.noteservice"
: sets the package name in the generated Kotlin/Java code.java_multiple_files = true
: instead of placing all generated types into a single file, this generates one file per message/service, which is cleaner and more modular.
The service
block
This defines the gRPC service and its RPC methods. In our case...
service NoteService {
rpc CreateNote (CreateNoteRequest) returns (Note);
}
... defines a service named NoteService
with one RPC method CreateNote
, which accepts a message of type CreateNoteRequest
and returns a Note
.
gRPC will generate both a base server class and a client stub for us, based on this.
The message
blocks
These define structured data for the request and response types.
message CreateNoteRequest {
string title = 1;
string content = 2;
}
This defines the input type for our CreateNote
method. It has two fields: title
(a string field with tag 1) and content
(another string field with tag 2).
And this...
message Note {
string id = 1;
string title = 2;
string content = 3;
}
... is the response type, which includes id
(a string representing the unique note ID, with tag 1), title
(for the note title with tag 2) and content
(for the note content with tag 3).
But what are those numbered tags?
Each field in a Protobuf message has a unique number. These numbers are used in the binary encoding of the message, not the field names. They matter a lot:
- Must not change once published — they define the wire format.
- Can't reuse deleted field numbers — unless you're absolutely sure they won't be interpreted by old clients.
- Field order doesn't matter in the source, but tags must be unique.
Protobuf tags must be between 1 and (229 - 1), but you should use 1–15 for frequently-used fields, since they encode more efficiently.
Step 2: Generating Kotlin code from Protobuf definitions
The next step is to generate Kotlin code from our .proto
file. There are two main ways to do this:
Option 1: Using Gradle
For compiling our Protobuf definitions to Kotlin code, we can use a Gradle plugin, which makes our life easier. Add the Protobuf plugin and dependencies to your build.gradle.kts
. Remember we also need to add the Kotlin Coroutines dependency:
plugins {
kotlin("jvm") version "1.9.22"
id("com.google.protobuf") version "0.9.4"
}
repositories {
mavenCentral()
}
dependencies {
implementation("io.grpc:grpc-kotlin-stub:1.4.1")
implementation("io.grpc:grpc-netty-shaded:1.71.0")
implementation("io.grpc:grpc-protobuf:1.71.0")
implementation("com.google.protobuf:protobuf-kotlin:4.30.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1")
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:4.30.2"
}
plugins {
create("grpc") {
artifact = "io.grpc:protoc-gen-grpc-java:1.71.0"
}
create("grpckt") {
artifact = "io.grpc:protoc-gen-grpc-kotlin:1.4.1:jdk8@jar"
}
}
generateProtoTasks {
all().forEach {
it.builtins {
create("kotlin")
}
it.plugins {
id("grpc")
id("grpckt")
}
}
}
}
(Consider using local String variables for dependency version numbers. I didn't do it here for simplicity.)
Now run:
./gradlew generateProto
This generates Kotlin and gRPC code under build/generated
.
Option 2: Manual compilation using protoc
You can also generate code manually:
protoc
--proto_path=src/main/proto
--kotlin_out=build/generated/source/proto/main/kotlin
--grpc-kotlin_out=build/generated/source/proto/main/kotlin
note_service.proto
You'll need the protoc
binary and the protoc-gen-grpc-kotlin
plugin installed on your system.
Step 3: Implementing the gRPC Server
Now that we have the Kotlin stub code generated, the next step is to write the backend logic that will handle incoming gRPC calls. This is where we implement the NoteService
.
package com.fugisawa.noteservice
import com.fugisawa.grpc.noteservice.CreateNoteRequest
import com.fugisawa.grpc.noteservice.Note
import com.fugisawa.grpc.noteservice.NoteServiceGrpcKt
import com.fugisawa.grpc.noteservice.note
import java.util.*
class NoteServiceImpl : NoteServiceGrpcKt.NoteServiceCoroutineImplBase() {
override suspend fun createNote(request: CreateNoteRequest): Note {
// Here, you will implement your note creation logic, persist the new note etc.
// Let's just generate dummy note, for this example: val newNoteId = UUID.randomUUID().toString()
return note {
id = newNoteId
title = request.title
content = request.content
}
}
}
In this code, what we are doing is:
- We are extending the
NoteServiceCoroutineImplBase
(generated by the Protobuf compilation). - We are implementing the
createNote()
function with our own logic for creating notes.
We also need to start our gRPC server, register the service, and block the main thread (which, for simplicity, we'll do in the main application entry point function):
package com.fugisawa.noteservice
import io.grpc.Server
import io.grpc.ServerBuilder
fun main() {
val server: Server = ServerBuilder
.forPort(50051)
.addService(NoteServiceImpl())
.build()
server.start()
println("Server started on port 50051")
server.awaitTermination()
}
That's all we need to get a Kotlin gRPC server running.
Step 4: Creating a gRPC Client in Kotlin
Now, let's now write a client that connects to the server and sends a CreateNote
request.
Typically, we will not implement a gRPC client for a gRPC service available in the same application. We will do this in this article just to keep things simple and avoid having to create a new application to act as the client.
package com.fugisawa.noteservice
import com.fugisawa.grpc.noteservice.NoteServiceGrpcKt.NoteServiceCoroutineStub
import com.fugisawa.grpc.noteservice.createNoteRequest
import io.grpc.ManagedChannelBuilder
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val channel =
ManagedChannelBuilder
.forAddress("localhost", 50051)
.usePlaintext()
.build()
val stub = NoteServiceCoroutineStub(channel)
val request = createNoteRequest {
title = "My first note"
content = "This is my first note created with gRPC!"
}
val note = stub.createNote(request)
println("Note created: ${note.id} - ${note.title}")
}
This small program connects to our gRPC server and calls the CreateNote
method, just like a real client would. Let's walk through what each part is doing.
We wrap our main
function in runBlocking
because gRPC Kotlin uses suspend functions for all RPC calls, which means we need a coroutine scope to call them.
fun main() = runBlocking { ... }
Then, we create a gRPC channel (which is a connection to the server). We are connecting to localhost:50051
, which is where our server is running. We are using .usePlaintext()
to disable TLS (which is fine for local development), as we didn't configure TLS for the server.
Then we create a stub. A stub is a client-side object that lets you call remote methods as if they were local. We're using the coroutine-based version, auto-generated by the gRPC Kotlin plugin.
val stub = NoteServiceCoroutineStub(channel)
This stub exposes suspend
functions like createNote()
that we can call directly.
The next thing we do is create a CreateNoteRequest
object using the Kotlin DSL builder generated from the .proto
file. This DSL-style builder only works because you're using the protobuf-kotlin
library and configured our Gradle setup to generate kotlin
output.
val request = createNoteRequest {
title = "My first note"
content = "This is my first note created with gRPC!"
}
Finally, here is where the actual RPC call happens. We call createNote()
on the stub, passing the request. It suspends while the request is sent and response is received from the server.
val note = stub.createNote(request)
Notice how simple and idiomatic calling a gRPC service can be in Kotlin:
- All calls are suspending functions — no callbacks, no threads to manage.
- Messages are constructed using type-safe Kotlin builders.
- Communication is binary, efficient, and strongly typed.
How to run the Server and the Client
We're building both server and client in the same Gradle project, but typically they would be two separate applications. In this article, we will use them by starting our application with different entry points.
- To run the server, execute
Server.main()
. - In a second terminal (or a new IntelliJ run configuration), run
Client.main()
.
Final thoughts
In this article, we covered:
- What gRPC and Protobuf are, and why they matter.
- When to use gRPC vs REST.
- How to define an API using a
.proto
file. - How to generate Kotlin code using Gradle and
protoc
. - How to implement and run a gRPC server and client in Kotlin.
In the upcoming articles, we'll dig deeper into optional
fields, field/argument presence detection, repeat
, map
and oneof
fields, and the subtle but important differences in how Protocol Buffers handle missing or default values. That's where things get interesting and real-world.
To explore more about Kotlin-related topics, subscribe to my newsletter at https://fugisawa.com/ and stay tuned for more insights and updates.