Implementing gRPC with Kotlin & Spring Boot 3: A Comprehensive Guide

In modern microservices, efficient and reliable inter-service communication is paramount. gRPC, an open-source Remote Procedure Call (RPC) framework developed by Google, leverages HTTP/2 and Protocol Buffers (protobuf) to provide high-performance, strongly-typed service interactions. This guide offers a detailed, step-by-step walkthrough of building a Todo application using Spring Boot 3, Spring gRPC (currently experimental), Kotlin, and MongoDB. We’ll implement the gRPC server first, followed by the client, ensuring a clear, sequential development process.

What is gRPC?

gRPC is a high-performance RPC framework that uses HTTP/2 for transport and Protocol Buffers for defining services and messages. It supports unary calls (single request-response) and streaming (client, server, or bidirectional), making it ideal for microservices requiring low latency and high throughput. Unlike REST, which typically uses JSON over HTTP/1.1, gRPC’s binary serialization and contract-first design enhance efficiency and reliability.

Benefits of gRPC Over REST

  1. Performance: HTTP/2’s multiplexing and header compression, paired with Protobuf’s compact binary format, outperform JSON-based REST APIs.
  2. Strong Typing: Protobuf enforces a schema, reducing runtime errors compared to REST’s loosely typed payloads.
  3. Streaming: gRPC supports real-time communication via streaming, unlike REST’s static request-response model.
  4. Code Generation: Protobuf generates client and server code across multiple languages, streamlining development.
  5. Interoperability: Its language-agnostic nature suits polyglot environments.

While REST is simpler and browser-friendly, gRPC excels in internal service communication for performance-critical systems.

Key Terms in gRPC

  • Protocol Buffers (protobuf): A serialization format and Interface Definition Language (IDL) for defining services and messages.
  • Service: A set of RPC methods (e.g., CreateTodo) defined in a .proto file.
  • Stub: Auto-generated code for clients to call server methods (e.g., TodoServiceGrpc.TodoServiceBlockingStub).
  • Channel: An abstraction over the HTTP/2 connection between client and server.
  • Unary RPC: A single request-response interaction, used in this guide.
  • Streaming RPC: Continuous data exchange, not covered here but supported by gRPC.

gRPC Server Implementation

Let’s build the grpc-server to manage Todo operations and persist data in MongoDB.

Step 1: Project Setup

  1. Create the Project:

    • Use Spring Initializr (start.spring.io) to generate a Spring Boot 3 project with Kotlin.
    • I have set the group to com.mahmud, artifact to grpc-server, and selected Kotlin as language, project type Gradle (Kotlin).
  2. Add Dependencies:

    • include:
      • spring-boot-starter-data-mongodb-reactive
      • spring-grpc-spring-boot-starter
  3. Configure MongoDB:

    • Ensure MongoDB is running locally mongodb://localhost:27017.
    • Create application.yaml:
    <span>spring</span><span>:</span>
    <span>application</span><span>:</span>
    <span>name</span><span>:</span> <span>grpc-server</span>
    <span>data</span><span>:</span>
    <span>mongodb</span><span>:</span>
    <span>uri</span><span>:</span> <span>mongodb://localhost:27017/todo_db</span>
     <span>spring</span><span>:</span>
       <span>application</span><span>:</span>
         <span>name</span><span>:</span> <span>grpc-server</span>
       <span>data</span><span>:</span>
         <span>mongodb</span><span>:</span>
           <span>uri</span><span>:</span> <span>mongodb://localhost:27017/todo_db</span>
    spring: application: name: grpc-server data: mongodb: uri: mongodb://localhost:27017/todo_db

Step 2: Define the Protobuf File

Create todo.proto to define the service and messages:

<span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span>
<span>package</span> <span>todo</span><span>;</span>
<span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span>
<span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcserver"</span><span>;</span>
<span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span>
<span>// The Todo service definition</span>
<span>service</span> <span>TodoService</span> <span>{</span>
<span>// Create a new todo item</span>
<span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
<span>// Get a todo item by ID</span>
<span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
<span>// List all todo items</span>
<span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span>
<span>// Update a todo item</span>
<span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
<span>// Delete a todo item</span>
<span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span>
<span>}</span>
<span>// Message definitions</span>
<span>message</span> <span>TodoItem</span> <span>{</span>
<span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
<span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
<span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
<span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span>
<span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span>
<span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span>
<span>}</span>
<span>message</span> <span>TodoRequest</span> <span>{</span>
<span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span> <span>// Empty for create, required for update</span>
<span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
<span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
<span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
<span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span>
<span>}</span>
<span>message</span> <span>TodoResponse</span> <span>{</span>
<span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>
<span>message</span> <span>TodoIdRequest</span> <span>{</span>
<span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>
<span>message</span> <span>ListTodosRequest</span> <span>{</span>
<span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span> <span>// Filter for completed todos</span>
<span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span> <span>// Number of items per page</span>
<span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span> <span>// Pagination token</span>
<span>}</span>
<span>message</span> <span>ListTodosResponse</span> <span>{</span>
<span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span>
<span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span>
<span>}</span>
<span>message</span> <span>Empty</span> <span>{}</span>
<span>// Import Google timestamp for timestamp fields</span>
<span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span>
<span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span>

<span>package</span> <span>todo</span><span>;</span>

<span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span>
<span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcserver"</span><span>;</span>
<span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span>

<span>// The Todo service definition</span>
<span>service</span> <span>TodoService</span> <span>{</span>
  <span>// Create a new todo item</span>
  <span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
  <span>// Get a todo item by ID</span>
  <span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
  <span>// List all todo items</span>
  <span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span>
  <span>// Update a todo item</span>
  <span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
  <span>// Delete a todo item</span>
  <span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span>
<span>}</span>

<span>// Message definitions</span>
<span>message</span> <span>TodoItem</span> <span>{</span>
  <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
  <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
  <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
  <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
  <span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span>
  <span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span>
  <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span>
<span>}</span>

<span>message</span> <span>TodoRequest</span> <span>{</span>
  <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>          <span>// Empty for create, required for update</span>
  <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
  <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
  <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
  <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span>
<span>}</span>

<span>message</span> <span>TodoResponse</span> <span>{</span>
  <span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>

<span>message</span> <span>TodoIdRequest</span> <span>{</span>
  <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>

<span>message</span> <span>ListTodosRequest</span> <span>{</span>
  <span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span>    <span>// Filter for completed todos</span>
  <span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span>                 <span>// Number of items per page</span>
  <span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span>              <span>// Pagination token</span>
<span>}</span>

<span>message</span> <span>ListTodosResponse</span> <span>{</span>
  <span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span>
  <span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span>
<span>}</span>

<span>message</span> <span>Empty</span> <span>{}</span>

<span>// Import Google timestamp for timestamp fields</span>
<span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span>
syntax = "proto3"; package todo; option java_multiple_files = true; option java_package = "com.mahmud.grpcserver"; option java_outer_classname = "TodoProto"; // The Todo service definition service TodoService { // Create a new todo item rpc CreateTodo (TodoRequest) returns (TodoResponse) {} // Get a todo item by ID rpc GetTodo (TodoIdRequest) returns (TodoResponse) {} // List all todo items rpc ListTodos (ListTodosRequest) returns (ListTodosResponse) {} // Update a todo item rpc UpdateTodo (TodoRequest) returns (TodoResponse) {} // Delete a todo item rpc DeleteTodo (TodoIdRequest) returns (Empty) {} } // Message definitions message TodoItem { string id = 1; string title = 2; string description = 3; bool completed = 4; google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp updated_at = 6; optional google.protobuf.Timestamp due_date = 7; } message TodoRequest { string id = 1; // Empty for create, required for update string title = 2; string description = 3; bool completed = 4; optional google.protobuf.Timestamp due_date = 5; } message TodoResponse { TodoItem todo = 1; } message TodoIdRequest { string id = 1; } message ListTodosRequest { optional bool show_completed = 1; // Filter for completed todos int32 page_size = 2; // Number of items per page string page_token = 3; // Pagination token } message ListTodosResponse { repeated TodoItem todos = 1; string next_page_token = 2; } message Empty {} // Import Google timestamp for timestamp fields import "google/protobuf/timestamp.proto";

Enter fullscreen mode Exit fullscreen mode

The proto defines the TodoService with five unary RPC methods for CRUD operations. Messages like TodoItem and TodoRequest use Protobuf scalars and Timestamp for date-time fields.

Breakdown:

  • service TodoService: Specifies CRUD methods.
  • TodoItem: Represents a Todo with fields like id and optional due_date.
  • repeated: Indicates a list (e.g., todos in ListTodosResponse).
  • optional: Marks nullable fields.

Step 3: Generate gRPC Stubs

Run:

./gradlew build
./gradlew build
./gradlew build

Enter fullscreen mode Exit fullscreen mode

This compiles the Protobuf file and generates stubs in build/generated/source/proto/main, including TodoServiceGrpc.java. In IntelliJ IDEA, right-click the generated folder and select “Mark Directory As → Generated Source Root”.

Step 4: Implement Server Logic

Create the domain model. It defines the Todo entity for MongoDB persistence.

Todo.kt

<span>package</span> <span>com.mahmud.grpcserver.model</span>
<span>import</span> <span>org.springframework.data.annotation.Id</span>
<span>import</span> <span>org.springframework.data.mongodb.core.mapping.Document</span>
<span>import</span> <span>java.time.Instant</span>
<span>@Document</span><span>(</span><span>collection</span> <span>=</span> <span>"todos"</span><span>)</span>
<span>data class</span> <span>Todo</span><span>(</span>
<span>@Id</span>
<span>val</span> <span>id</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>,</span>
<span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span>
<span>val</span> <span>createdAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span>
<span>val</span> <span>updatedAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span>
<span>val</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?</span> <span>=</span> <span>null</span>
<span>)</span>
<span>package</span> <span>com.mahmud.grpcserver.model</span>

<span>import</span> <span>org.springframework.data.annotation.Id</span>
<span>import</span> <span>org.springframework.data.mongodb.core.mapping.Document</span>
<span>import</span> <span>java.time.Instant</span>

<span>@Document</span><span>(</span><span>collection</span> <span>=</span> <span>"todos"</span><span>)</span>
<span>data class</span> <span>Todo</span><span>(</span>
    <span>@Id</span>
    <span>val</span> <span>id</span><span>:</span> <span>String</span><span>?</span> <span>=</span> <span>null</span><span>,</span>
    <span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span>
    <span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span>
    <span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span>
    <span>val</span> <span>createdAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span>
    <span>val</span> <span>updatedAt</span><span>:</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>(),</span>
    <span>val</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?</span> <span>=</span> <span>null</span>
<span>)</span>
package com.mahmud.grpcserver.model import org.springframework.data.annotation.Id import org.springframework.data.mongodb.core.mapping.Document import java.time.Instant @Document(collection = "todos") data class Todo( @Id val id: String? = null, val title: String, val description: String, val completed: Boolean = false, val createdAt: Instant = Instant.now(), val updatedAt: Instant = Instant.now(), val dueDate: Instant? = null )

Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • @Document(collection = "todos"): Maps to the todos collection.
  • @Id: Unique identifier, nullable for new entries.
  • Instant: Matches Protobuf’s Timestamp.

Create the repository interface. It provides reactive CRUD operations with a custom filter method.

TodoRepository.kt

<span>package</span> <span>com.mahmud.grpcserver.repository</span>
<span>import</span> <span>com.mahmud.grpcserver.model.Todo</span>
<span>import</span> <span>org.springframework.data.mongodb.repository.ReactiveMongoRepository</span>
<span>import</span> <span>reactor.core.publisher.Flux</span>
<span>interface</span> <span>TodoRepository</span> <span>:</span> <span>ReactiveMongoRepository</span><span><</span><span>Todo</span><span>,</span> <span>String</span><span>></span> <span>{</span>
<span>fun</span> <span>findByCompleted</span><span>(</span><span>completed</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>Todo</span><span>></span>
<span>}</span>
<span>package</span> <span>com.mahmud.grpcserver.repository</span>

<span>import</span> <span>com.mahmud.grpcserver.model.Todo</span>
<span>import</span> <span>org.springframework.data.mongodb.repository.ReactiveMongoRepository</span>
<span>import</span> <span>reactor.core.publisher.Flux</span>

<span>interface</span> <span>TodoRepository</span> <span>:</span> <span>ReactiveMongoRepository</span><span><</span><span>Todo</span><span>,</span> <span>String</span><span>></span> <span>{</span>
    <span>fun</span> <span>findByCompleted</span><span>(</span><span>completed</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>Todo</span><span>></span>
<span>}</span>
package com.mahmud.grpcserver.repository import com.mahmud.grpcserver.model.Todo import org.springframework.data.mongodb.repository.ReactiveMongoRepository import reactor.core.publisher.Flux interface TodoRepository : ReactiveMongoRepository<Todo, String> { fun findByCompleted(completed: Boolean): Flux<Todo> }

Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • ReactiveMongoRepository<Todo, String>: Manages Todo entities.
  • Flux<Todo>: Returns a reactive stream.

Implement the gRPC service. It will handle our main logics.

TodoService.kt

<span>package</span> <span>com.mahmud.grpcserver.service</span>
<span>import</span> <span>com.mahmud.grpcserver.model.Todo</span>
<span>import</span> <span>com.google.protobuf.Timestamp</span>
<span>import</span> <span>com.mahmud.grpcserver.*</span>
<span>import</span> <span>com.mahmud.grpcserver.repository.TodoRepository</span>
<span>import</span> <span>io.grpc.stub.StreamObserver</span>
<span>import</span> <span>org.springframework.beans.factory.annotation.Autowired</span>
<span>import</span> <span>org.springframework.grpc.server.service.GrpcService</span>
<span>import</span> <span>reactor.core.publisher.Mono</span>
<span>import</span> <span>java.time.Instant</span>
<span>@GrpcService</span>
<span>class</span> <span>TodoService</span> <span>@Autowired</span> <span>constructor</span><span>(</span>
<span>private</span> <span>val</span> <span>todoRepository</span><span>:</span> <span>TodoRepository</span>
<span>)</span> <span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceImplBase</span><span>()</span> <span>{</span>
<span>// gRPC method to create a new Todo item</span>
<span>override</span> <span>fun</span> <span>createTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span>
<span>val</span> <span>todo</span> <span>=</span> <span>Todo</span><span>(</span>
<span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span>
<span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span>
<span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span>
<span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>()</span> <span>// Convert protobuf Timestamp to Java Instant</span>
<span>)</span>
<span>// Save Todo to database using reactive programming</span>
<span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>todo</span><span>)</span>
<span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span>
<span>.</span><span>subscribe</span><span>(</span>
<span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
<span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
<span>)</span>
<span>}</span>
<span>// gRPC method to retrieve a Todo by its ID</span>
<span>override</span> <span>fun</span> <span>getTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span>
<span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span>
<span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span>
<span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span>
<span>.</span><span>subscribe</span><span>(</span>
<span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
<span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
<span>)</span>
<span>}</span>
<span>// gRPC method to list all Todos with an optional filter for completed ones</span>
<span>override</span> <span>fun</span> <span>listTodos</span><span>(</span><span>request</span><span>:</span> <span>ListTodosRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>ListTodosResponse</span><span>>)</span> <span>{</span>
<span>val</span> <span>todos</span> <span>=</span> <span>if</span> <span>(</span><span>request</span><span>.</span><span>showCompleted</span><span>)</span> <span>{</span>
<span>todoRepository</span><span>.</span><span>findAll</span><span>()</span> <span>// Fetch all Todos</span>
<span>}</span> <span>else</span> <span>{</span>
<span>todoRepository</span><span>.</span><span>findByCompleted</span><span>(</span><span>false</span><span>)</span> <span>// Fetch only incomplete Todos</span>
<span>}</span>
<span>todos</span><span>.</span><span>collectList</span><span>()</span>
<span>.</span><span>map</span> <span>{</span> <span>items</span> <span>-></span>
<span>ListTodosResponse</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>addAllTodos</span><span>(</span><span>items</span><span>.</span><span>map</span> <span>{</span> <span>it</span><span>.</span><span>toProto</span><span>()</span> <span>})</span> <span>// Convert to protobuf format</span>
<span>.</span><span>build</span><span>()</span>
<span>}</span>
<span>.</span><span>subscribe</span><span>(</span>
<span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
<span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
<span>)</span>
<span>}</span>
<span>// gRPC method to update an existing Todo item</span>
<span>override</span> <span>fun</span> <span>updateTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span>
<span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span>
<span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span>
<span>.</span><span>map</span> <span>{</span> <span>existing</span> <span>-></span>
<span>existing</span><span>.</span><span>copy</span><span>(</span>
<span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span>
<span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span>
<span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span>
<span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>(),</span>
<span>updatedAt</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>()</span> <span>// Update timestamp</span>
<span>)</span>
<span>}</span>
<span>.</span><span>flatMap</span> <span>{</span> <span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>it</span><span>)</span> <span>}</span> <span>// Save updated Todo</span>
<span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span>
<span>.</span><span>subscribe</span><span>(</span>
<span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
<span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
<span>)</span>
<span>}</span>
<span>// gRPC method to delete a Todo item by its ID</span>
<span>override</span> <span>fun</span> <span>deleteTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>?,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>Empty</span><span>>?)</span> <span>{</span>
<span>request</span><span>?.</span><span>let</span> <span>{</span>
<span>todoRepository</span><span>.</span><span>deleteById</span><span>(</span><span>it</span><span>.</span><span>id</span><span>)</span>
<span>.</span><span>then</span><span>(</span><span>Mono</span><span>.</span><span>just</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>()))</span> <span>// Return an empty response</span>
<span>.</span><span>subscribe</span><span>(</span>
<span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span> <span>},</span>
<span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
<span>)</span>
<span>}</span> <span>?:</span> <span>run</span> <span>{</span>
<span>// If request is null, complete the response</span>
<span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>())</span>
<span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span>
<span>}</span>
<span>}</span>
<span>}</span>
<span>// Extension function to convert a Todo entity to a gRPC TodoItem message</span>
<span>fun</span> <span>Todo</span><span>.</span><span>toProto</span><span>():</span> <span>TodoItem</span> <span>{</span>
<span>val</span> <span>builder</span> <span>=</span> <span>TodoItem</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setId</span><span>(</span><span>id</span><span>!!</span><span>)</span>
<span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span>
<span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span>
<span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span>
<span>.</span><span>setCreatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setSeconds</span><span>(</span><span>createdAt</span><span>.</span><span>epochSecond</span><span>)</span>
<span>.</span><span>setNanos</span><span>(</span><span>createdAt</span><span>.</span><span>nano</span><span>)</span>
<span>.</span><span>build</span><span>())</span>
<span>.</span><span>setUpdatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setSeconds</span><span>(</span><span>updatedAt</span><span>.</span><span>epochSecond</span><span>)</span>
<span>.</span><span>setNanos</span><span>(</span><span>updatedAt</span><span>.</span><span>nano</span><span>)</span>
<span>.</span><span>build</span><span>())</span>
<span>dueDate</span><span>?.</span><span>let</span> <span>{</span>
<span>builder</span><span>.</span><span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span>
<span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span>
<span>.</span><span>build</span><span>())</span>
<span>}</span>
<span>return</span> <span>builder</span><span>.</span><span>build</span><span>()</span>
<span>}</span>
<span>// Extension function to convert a gRPC Timestamp to Java Instant</span>
<span>fun</span> <span>Timestamp</span><span>.</span><span>toInstant</span><span>():</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>seconds</span><span>,</span> <span>nanos</span><span>.</span><span>toLong</span><span>())</span>
<span>package</span> <span>com.mahmud.grpcserver.service</span>

<span>import</span> <span>com.mahmud.grpcserver.model.Todo</span>
<span>import</span> <span>com.google.protobuf.Timestamp</span>
<span>import</span> <span>com.mahmud.grpcserver.*</span>
<span>import</span> <span>com.mahmud.grpcserver.repository.TodoRepository</span>
<span>import</span> <span>io.grpc.stub.StreamObserver</span>
<span>import</span> <span>org.springframework.beans.factory.annotation.Autowired</span>
<span>import</span> <span>org.springframework.grpc.server.service.GrpcService</span>
<span>import</span> <span>reactor.core.publisher.Mono</span>
<span>import</span> <span>java.time.Instant</span>

<span>@GrpcService</span>
<span>class</span> <span>TodoService</span> <span>@Autowired</span> <span>constructor</span><span>(</span>
   <span>private</span> <span>val</span> <span>todoRepository</span><span>:</span> <span>TodoRepository</span>
<span>)</span> <span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceImplBase</span><span>()</span> <span>{</span>

   <span>// gRPC method to create a new Todo item</span>
   <span>override</span> <span>fun</span> <span>createTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span>
      <span>val</span> <span>todo</span> <span>=</span> <span>Todo</span><span>(</span>
         <span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span>
         <span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span>
         <span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span>
         <span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>()</span> <span>// Convert protobuf Timestamp to Java Instant</span>
      <span>)</span>
      <span>// Save Todo to database using reactive programming</span>
      <span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>todo</span><span>)</span>
         <span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span>
         <span>.</span><span>subscribe</span><span>(</span>
            <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
            <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
         <span>)</span>
   <span>}</span>

   <span>// gRPC method to retrieve a Todo by its ID</span>
   <span>override</span> <span>fun</span> <span>getTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span>
      <span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span>
         <span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span>
         <span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span>
         <span>.</span><span>subscribe</span><span>(</span>
            <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
            <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
         <span>)</span>
   <span>}</span>

   <span>// gRPC method to list all Todos with an optional filter for completed ones</span>
   <span>override</span> <span>fun</span> <span>listTodos</span><span>(</span><span>request</span><span>:</span> <span>ListTodosRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>ListTodosResponse</span><span>>)</span> <span>{</span>
      <span>val</span> <span>todos</span> <span>=</span> <span>if</span> <span>(</span><span>request</span><span>.</span><span>showCompleted</span><span>)</span> <span>{</span>
         <span>todoRepository</span><span>.</span><span>findAll</span><span>()</span> <span>// Fetch all Todos</span>
      <span>}</span> <span>else</span> <span>{</span>
         <span>todoRepository</span><span>.</span><span>findByCompleted</span><span>(</span><span>false</span><span>)</span> <span>// Fetch only incomplete Todos</span>
      <span>}</span>
      <span>todos</span><span>.</span><span>collectList</span><span>()</span>
         <span>.</span><span>map</span> <span>{</span> <span>items</span> <span>-></span>
            <span>ListTodosResponse</span><span>.</span><span>newBuilder</span><span>()</span>
               <span>.</span><span>addAllTodos</span><span>(</span><span>items</span><span>.</span><span>map</span> <span>{</span> <span>it</span><span>.</span><span>toProto</span><span>()</span> <span>})</span> <span>// Convert to protobuf format</span>
               <span>.</span><span>build</span><span>()</span>
         <span>}</span>
         <span>.</span><span>subscribe</span><span>(</span>
            <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
            <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
         <span>)</span>
   <span>}</span>

   <span>// gRPC method to update an existing Todo item</span>
   <span>override</span> <span>fun</span> <span>updateTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoRequest</span><span>,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>TodoResponse</span><span>>)</span> <span>{</span>
      <span>todoRepository</span><span>.</span><span>findById</span><span>(</span><span>request</span><span>.</span><span>id</span><span>)</span>
         <span>.</span><span>switchIfEmpty</span><span>(</span><span>Mono</span><span>.</span><span>error</span><span>(</span><span>RuntimeException</span><span>(</span><span>"Todo not found"</span><span>)))</span> <span>// Handle missing Todo</span>
         <span>.</span><span>map</span> <span>{</span> <span>existing</span> <span>-></span>
            <span>existing</span><span>.</span><span>copy</span><span>(</span>
               <span>title</span> <span>=</span> <span>request</span><span>.</span><span>title</span><span>,</span>
               <span>description</span> <span>=</span> <span>request</span><span>.</span><span>description</span><span>,</span>
               <span>completed</span> <span>=</span> <span>request</span><span>.</span><span>completed</span><span>,</span>
               <span>dueDate</span> <span>=</span> <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>toInstant</span><span>(),</span>
               <span>updatedAt</span> <span>=</span> <span>Instant</span><span>.</span><span>now</span><span>()</span> <span>// Update timestamp</span>
            <span>)</span>
         <span>}</span>
         <span>.</span><span>flatMap</span> <span>{</span> <span>todoRepository</span><span>.</span><span>save</span><span>(</span><span>it</span><span>)</span> <span>}</span> <span>// Save updated Todo</span>
         <span>.</span><span>map</span> <span>{</span> <span>TodoResponse</span><span>.</span><span>newBuilder</span><span>().</span><span>setTodo</span><span>(</span><span>it</span><span>.</span><span>toProto</span><span>()).</span><span>build</span><span>()</span> <span>}</span>
         <span>.</span><span>subscribe</span><span>(</span>
            <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>.</span><span>onCompleted</span><span>()</span> <span>},</span>
            <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
         <span>)</span>
   <span>}</span>

   <span>// gRPC method to delete a Todo item by its ID</span>
   <span>override</span> <span>fun</span> <span>deleteTodo</span><span>(</span><span>request</span><span>:</span> <span>TodoIdRequest</span><span>?,</span> <span>responseObserver</span><span>:</span> <span>StreamObserver</span><span><</span><span>Empty</span><span>>?)</span> <span>{</span>
      <span>request</span><span>?.</span><span>let</span> <span>{</span>
         <span>todoRepository</span><span>.</span><span>deleteById</span><span>(</span><span>it</span><span>.</span><span>id</span><span>)</span>
            <span>.</span><span>then</span><span>(</span><span>Mono</span><span>.</span><span>just</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>()))</span> <span>// Return an empty response</span>
            <span>.</span><span>subscribe</span><span>(</span>
               <span>{</span> <span>response</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>response</span><span>);</span> <span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span> <span>},</span>
               <span>{</span> <span>error</span> <span>-></span> <span>responseObserver</span><span>?.</span><span>onError</span><span>(</span><span>error</span><span>)</span> <span>}</span>
            <span>)</span>
      <span>}</span> <span>?:</span> <span>run</span> <span>{</span>
         <span>// If request is null, complete the response</span>
         <span>responseObserver</span><span>?.</span><span>onNext</span><span>(</span><span>Empty</span><span>.</span><span>newBuilder</span><span>().</span><span>build</span><span>())</span>
         <span>responseObserver</span><span>?.</span><span>onCompleted</span><span>()</span>
      <span>}</span>
   <span>}</span>
<span>}</span>

<span>// Extension function to convert a Todo entity to a gRPC TodoItem message</span>
<span>fun</span> <span>Todo</span><span>.</span><span>toProto</span><span>():</span> <span>TodoItem</span> <span>{</span>
   <span>val</span> <span>builder</span> <span>=</span> <span>TodoItem</span><span>.</span><span>newBuilder</span><span>()</span>
      <span>.</span><span>setId</span><span>(</span><span>id</span><span>!!</span><span>)</span>
      <span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span>
      <span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span>
      <span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span>
      <span>.</span><span>setCreatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
         <span>.</span><span>setSeconds</span><span>(</span><span>createdAt</span><span>.</span><span>epochSecond</span><span>)</span>
         <span>.</span><span>setNanos</span><span>(</span><span>createdAt</span><span>.</span><span>nano</span><span>)</span>
         <span>.</span><span>build</span><span>())</span>
      <span>.</span><span>setUpdatedAt</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
         <span>.</span><span>setSeconds</span><span>(</span><span>updatedAt</span><span>.</span><span>epochSecond</span><span>)</span>
         <span>.</span><span>setNanos</span><span>(</span><span>updatedAt</span><span>.</span><span>nano</span><span>)</span>
         <span>.</span><span>build</span><span>())</span>
   <span>dueDate</span><span>?.</span><span>let</span> <span>{</span>
      <span>builder</span><span>.</span><span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
         <span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span>
         <span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span>
         <span>.</span><span>build</span><span>())</span>
   <span>}</span>
   <span>return</span> <span>builder</span><span>.</span><span>build</span><span>()</span>
<span>}</span>

<span>// Extension function to convert a gRPC Timestamp to Java Instant</span>
<span>fun</span> <span>Timestamp</span><span>.</span><span>toInstant</span><span>():</span> <span>Instant</span> <span>=</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>seconds</span><span>,</span> <span>nanos</span><span>.</span><span>toLong</span><span>())</span>
package com.mahmud.grpcserver.service import com.mahmud.grpcserver.model.Todo import com.google.protobuf.Timestamp import com.mahmud.grpcserver.* import com.mahmud.grpcserver.repository.TodoRepository import io.grpc.stub.StreamObserver import org.springframework.beans.factory.annotation.Autowired import org.springframework.grpc.server.service.GrpcService import reactor.core.publisher.Mono import java.time.Instant @GrpcService class TodoService @Autowired constructor( private val todoRepository: TodoRepository ) : TodoServiceGrpc.TodoServiceImplBase() { // gRPC method to create a new Todo item override fun createTodo(request: TodoRequest, responseObserver: StreamObserver<TodoResponse>) { val todo = Todo( title = request.title, description = request.description, completed = request.completed, dueDate = request.dueDate?.toInstant() // Convert protobuf Timestamp to Java Instant ) // Save Todo to database using reactive programming todoRepository.save(todo) .map { TodoResponse.newBuilder().setTodo(it.toProto()).build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to retrieve a Todo by its ID override fun getTodo(request: TodoIdRequest, responseObserver: StreamObserver<TodoResponse>) { todoRepository.findById(request.id) .switchIfEmpty(Mono.error(RuntimeException("Todo not found"))) // Handle missing Todo .map { TodoResponse.newBuilder().setTodo(it.toProto()).build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to list all Todos with an optional filter for completed ones override fun listTodos(request: ListTodosRequest, responseObserver: StreamObserver<ListTodosResponse>) { val todos = if (request.showCompleted) { todoRepository.findAll() // Fetch all Todos } else { todoRepository.findByCompleted(false) // Fetch only incomplete Todos } todos.collectList() .map { items -> ListTodosResponse.newBuilder() .addAllTodos(items.map { it.toProto() }) // Convert to protobuf format .build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to update an existing Todo item override fun updateTodo(request: TodoRequest, responseObserver: StreamObserver<TodoResponse>) { todoRepository.findById(request.id) .switchIfEmpty(Mono.error(RuntimeException("Todo not found"))) // Handle missing Todo .map { existing -> existing.copy( title = request.title, description = request.description, completed = request.completed, dueDate = request.dueDate?.toInstant(), updatedAt = Instant.now() // Update timestamp ) } .flatMap { todoRepository.save(it) } // Save updated Todo .map { TodoResponse.newBuilder().setTodo(it.toProto()).build() } .subscribe( { response -> responseObserver.onNext(response); responseObserver.onCompleted() }, { error -> responseObserver.onError(error) } ) } // gRPC method to delete a Todo item by its ID override fun deleteTodo(request: TodoIdRequest?, responseObserver: StreamObserver<Empty>?) { request?.let { todoRepository.deleteById(it.id) .then(Mono.just(Empty.newBuilder().build())) // Return an empty response .subscribe( { response -> responseObserver?.onNext(response); responseObserver?.onCompleted() }, { error -> responseObserver?.onError(error) } ) } ?: run { // If request is null, complete the response responseObserver?.onNext(Empty.newBuilder().build()) responseObserver?.onCompleted() } } } // Extension function to convert a Todo entity to a gRPC TodoItem message fun Todo.toProto(): TodoItem { val builder = TodoItem.newBuilder() .setId(id!!) .setTitle(title) .setDescription(description) .setCompleted(completed) .setCreatedAt(Timestamp.newBuilder() .setSeconds(createdAt.epochSecond) .setNanos(createdAt.nano) .build()) .setUpdatedAt(Timestamp.newBuilder() .setSeconds(updatedAt.epochSecond) .setNanos(updatedAt.nano) .build()) dueDate?.let { builder.setDueDate(Timestamp.newBuilder() .setSeconds(it.epochSecond) .setNanos(it.nano) .build()) } return builder.build() } // Extension function to convert a gRPC Timestamp to Java Instant fun Timestamp.toInstant(): Instant = Instant.ofEpochSecond(seconds, nanos.toLong())

Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • @GrpcService: Integrates with Spring gRPC.
  • Comments explain gRPC request handling, response building, and call completion.

Step 5: Run the Server

Ensure MongoDB is running.

Run:

./gradlew bootRun
./gradlew bootRun
./gradlew bootRun

Enter fullscreen mode Exit fullscreen mode

The gRPC server listens on localhost:9090.

Let’s test the gGRPC Server using grpcurl

grpcurl is a command-line tool for interacting with gRPC servers. These commands assume the server supports reflection (common in development) or that you have the todo.proto file locally. If reflection isn’t enabled, add -proto todo.proto to each command and ensure the file is in your working directory.

1. Create Todo

Creates a new Todo item.

grpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "due_date": {"seconds": 1735689600, "nanos": 0} }'</span> localhost:9090 todo.TodoService/CreateTodo
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "due_date": {"seconds": 1735689600, "nanos": 0} }'</span> localhost:9090 todo.TodoService/CreateTodo
grpcurl -plaintext -d '{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "due_date": {"seconds": 1735689600, "nanos": 0} }' localhost:9090 todo.TodoService/CreateTodo

Enter fullscreen mode Exit fullscreen mode

  • -plaintext: No TLS (matches server config).
  • -d: JSON payload for the TodoRequest message.
  • due_date: Set to a future timestamp (e.g., Dec 31, 2025, 00:00:00 UTC).
  • Expected response: TodoResponse with the created TodoItem.

2. Get Todo

Retrieves a Todo by ID (replace <todo-id> with an actual ID from a prior CreateTodo response).

grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/GetTodo
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/GetTodo
grpcurl -plaintext -d '{"id": "<todo-id>"}' localhost:9090 todo.TodoService/GetTodo

Enter fullscreen mode Exit fullscreen mode

  • Payload: TodoIdRequest with the Todo’s ID.
  • Expected response: TodoResponse with the matching TodoItem.

3. List Todos

Lists all Todo items, optionally filtering by completion status.

grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"show_completed": true, "page_size": 10}'</span> localhost:9090 todo.TodoService/ListTodos
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"show_completed": true, "page_size": 10}'</span> localhost:9090 todo.TodoService/ListTodos
grpcurl -plaintext -d '{"show_completed": true, "page_size": 10}' localhost:9090 todo.TodoService/ListTodos

Enter fullscreen mode Exit fullscreen mode

  • show_completed: true to include completed Todos; set to false to filter them out.
  • page_size: Limits results (e.g., 10 items).
  • Expected response: ListTodosResponse with a list of TodoItems.

4. Update Todo

Updates an existing Todo (replace <todo-id> with an actual ID).

grpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "id": "<todo-id>", "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "due_date": {"seconds": 1735776000, "nanos": 0} }'</span> localhost:9090 todo.TodoService/UpdateTodo
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{ "id": "<todo-id>", "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "due_date": {"seconds": 1735776000, "nanos": 0} }'</span> localhost:9090 todo.TodoService/UpdateTodo
grpcurl -plaintext -d '{ "id": "<todo-id>", "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "due_date": {"seconds": 1735776000, "nanos": 0} }' localhost:9090 todo.TodoService/UpdateTodo

Enter fullscreen mode Exit fullscreen mode

  • Payload: TodoRequest with updated fields.
  • due_date: Updated to Jan 1, 2026, 00:00:00 UTC.
  • Expected response: TodoResponse with the updated TodoItem.

5. Delete Todo

Deletes a Todo by ID (replace <todo-id> with an actual ID).

grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/DeleteTodo
grpcurl <span>-plaintext</span> <span>-d</span> <span>'{"id": "<todo-id>"}'</span> localhost:9090 todo.TodoService/DeleteTodo
grpcurl -plaintext -d '{"id": "<todo-id>"}' localhost:9090 todo.TodoService/DeleteTodo

Enter fullscreen mode Exit fullscreen mode

  • Payload: TodoIdRequest with the Todo’s ID.
  • Expected response: Empty message ({}).

gRPC Client Implementation

Now, let’s build the grpc-client to expose REST endpoints and interact with the server.

Step 1: Project Setup

  1. Create the Project:

    • Use Spring Initializr to generate a Spring Boot 3 project with Kotlin.
    • In my case, I have set the group to com.mahmud, artifact to grpc-client.
  2. Add Dependencies:

    • Include the following dependencies:
    <span>org</span><span>.</span><span>springframework</span><span>.</span><span>boot</span><span>:</span><span>spring-boot-starter-webflux</span>
    <span>org</span><span>.</span><span>springframework</span><span>.</span><span>grpc</span><span>:</span><span>spring-grpc-spring-boot-starter</span>
       <span>org</span><span>.</span><span>springframework</span><span>.</span><span>boot</span><span>:</span><span>spring-boot-starter-webflux</span>
       <span>org</span><span>.</span><span>springframework</span><span>.</span><span>grpc</span><span>:</span><span>spring-grpc-spring-boot-starter</span>
    org.springframework.boot:spring-boot-starter-webflux org.springframework.grpc:spring-grpc-spring-boot-starter
  3. Configure Application:

    • Create application.yaml:
    <span>spring</span><span>:</span>
    <span>application</span><span>:</span>
    <span>name</span><span>:</span> <span>grpc-client</span>
    <span>grpc</span><span>:</span>
    <span>server</span><span>:</span>
    <span>port</span><span>:</span> <span>9091</span>
     <span>spring</span><span>:</span>
       <span>application</span><span>:</span>
         <span>name</span><span>:</span> <span>grpc-client</span>
       <span>grpc</span><span>:</span>
         <span>server</span><span>:</span>
           <span>port</span><span>:</span> <span>9091</span>
    spring: application: name: grpc-client grpc: server: port: 9091

Step 2: Define the Protobuf File

Create todo.proto file in src/main/proto directory. It defines the client’s TodoService, matching the server’s contract with a different package. Identical to the server’s todo.proto except for java_package.

<span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span>
<span>package</span> <span>todo</span><span>;</span>
<span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span>
<span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcclient"</span><span>;</span>
<span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span>
<span>service</span> <span>TodoService</span> <span>{</span>
<span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
<span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
<span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span>
<span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
<span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span>
<span>}</span>
<span>message</span> <span>TodoItem</span> <span>{</span>
<span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
<span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
<span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
<span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span>
<span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span>
<span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span>
<span>}</span>
<span>message</span> <span>TodoRequest</span> <span>{</span>
<span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
<span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
<span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
<span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span>
<span>}</span>
<span>message</span> <span>TodoResponse</span> <span>{</span>
<span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>
<span>message</span> <span>TodoIdRequest</span> <span>{</span>
<span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>
<span>message</span> <span>ListTodosRequest</span> <span>{</span>
<span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span>
<span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span>
<span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span>
<span>}</span>
<span>message</span> <span>ListTodosResponse</span> <span>{</span>
<span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span>
<span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span>
<span>}</span>
<span>message</span> <span>Empty</span> <span>{}</span>
<span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span>
<span>syntax</span> <span>=</span> <span>"proto3"</span><span>;</span>

<span>package</span> <span>todo</span><span>;</span>

<span>option</span> <span>java_multiple_files</span> <span>=</span> <span>true</span><span>;</span>
<span>option</span> <span>java_package</span> <span>=</span> <span>"com.mahmud.grpcclient"</span><span>;</span>
<span>option</span> <span>java_outer_classname</span> <span>=</span> <span>"TodoProto"</span><span>;</span>

<span>service</span> <span>TodoService</span> <span>{</span>
   <span>rpc</span> <span>CreateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
   <span>rpc</span> <span>GetTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
   <span>rpc</span> <span>ListTodos</span> <span>(</span><span>ListTodosRequest</span><span>)</span> <span>returns</span> <span>(</span><span>ListTodosResponse</span><span>)</span> <span>{}</span>
   <span>rpc</span> <span>UpdateTodo</span> <span>(</span><span>TodoRequest</span><span>)</span> <span>returns</span> <span>(</span><span>TodoResponse</span><span>)</span> <span>{}</span>
   <span>rpc</span> <span>DeleteTodo</span> <span>(</span><span>TodoIdRequest</span><span>)</span> <span>returns</span> <span>(</span><span>Empty</span><span>)</span> <span>{}</span>
<span>}</span>

<span>message</span> <span>TodoItem</span> <span>{</span>
   <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
   <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
   <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
   <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
   <span>google.protobuf.Timestamp</span> <span>created_at</span> <span>=</span> <span>5</span><span>;</span>
   <span>google.protobuf.Timestamp</span> <span>updated_at</span> <span>=</span> <span>6</span><span>;</span>
   <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>7</span><span>;</span>
<span>}</span>

<span>message</span> <span>TodoRequest</span> <span>{</span>
   <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
   <span>string</span> <span>title</span> <span>=</span> <span>2</span><span>;</span>
   <span>string</span> <span>description</span> <span>=</span> <span>3</span><span>;</span>
   <span>bool</span> <span>completed</span> <span>=</span> <span>4</span><span>;</span>
   <span>optional</span> <span>google.protobuf.Timestamp</span> <span>due_date</span> <span>=</span> <span>5</span><span>;</span>
<span>}</span>

<span>message</span> <span>TodoResponse</span> <span>{</span>
   <span>TodoItem</span> <span>todo</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>

<span>message</span> <span>TodoIdRequest</span> <span>{</span>
   <span>string</span> <span>id</span> <span>=</span> <span>1</span><span>;</span>
<span>}</span>

<span>message</span> <span>ListTodosRequest</span> <span>{</span>
   <span>optional</span> <span>bool</span> <span>show_completed</span> <span>=</span> <span>1</span><span>;</span>
   <span>int32</span> <span>page_size</span> <span>=</span> <span>2</span><span>;</span>
   <span>string</span> <span>page_token</span> <span>=</span> <span>3</span><span>;</span>
<span>}</span>

<span>message</span> <span>ListTodosResponse</span> <span>{</span>
   <span>repeated</span> <span>TodoItem</span> <span>todos</span> <span>=</span> <span>1</span><span>;</span>
   <span>string</span> <span>next_page_token</span> <span>=</span> <span>2</span><span>;</span>
<span>}</span>

<span>message</span> <span>Empty</span> <span>{}</span>

<span>import</span> <span>"google/protobuf/timestamp.proto"</span><span>;</span>
syntax = "proto3"; package todo; option java_multiple_files = true; option java_package = "com.mahmud.grpcclient"; option java_outer_classname = "TodoProto"; service TodoService { rpc CreateTodo (TodoRequest) returns (TodoResponse) {} rpc GetTodo (TodoIdRequest) returns (TodoResponse) {} rpc ListTodos (ListTodosRequest) returns (ListTodosResponse) {} rpc UpdateTodo (TodoRequest) returns (TodoResponse) {} rpc DeleteTodo (TodoIdRequest) returns (Empty) {} } message TodoItem { string id = 1; string title = 2; string description = 3; bool completed = 4; google.protobuf.Timestamp created_at = 5; google.protobuf.Timestamp updated_at = 6; optional google.protobuf.Timestamp due_date = 7; } message TodoRequest { string id = 1; string title = 2; string description = 3; bool completed = 4; optional google.protobuf.Timestamp due_date = 5; } message TodoResponse { TodoItem todo = 1; } message TodoIdRequest { string id = 1; } message ListTodosRequest { optional bool show_completed = 1; int32 page_size = 2; string page_token = 3; } message ListTodosResponse { repeated TodoItem todos = 1; string next_page_token = 2; } message Empty {} import "google/protobuf/timestamp.proto";

Enter fullscreen mode Exit fullscreen mode

Step 3: Generate gRPC Stubs

Run:

./gradlew build
./gradlew build
./gradlew build

Enter fullscreen mode Exit fullscreen mode

This generates stubs in build/generated/source/proto/main. Mark as a generated source root in your IDE.

Step 4: Implement Client Logic

Let’s configure the gRPC client according to the grpc-server.

GrpcClientConfig.kt

<span>package</span> <span>com.mahmud.grpcclient.config</span>
<span>import</span> <span>com.mahmud.grpcclient.TodoServiceGrpc</span>
<span>import</span> <span>io.grpc.ManagedChannel</span>
<span>import</span> <span>io.grpc.ManagedChannelBuilder</span>
<span>import</span> <span>org.springframework.context.annotation.Bean</span>
<span>import</span> <span>org.springframework.context.annotation.Configuration</span>
<span>@Configuration</span>
<span>class</span> <span>GrpcClientConfig</span> <span>{</span>
<span>@Bean</span>
<span>fun</span> <span>todoServiceStub</span><span>():</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span> <span>{</span>
<span>val</span> <span>channel</span><span>:</span> <span>ManagedChannel</span> <span>=</span> <span>ManagedChannelBuilder</span>
<span>.</span><span>forAddress</span><span>(</span><span>"localhost"</span><span>,</span> <span>9090</span><span>)</span> <span>// Connects to our gRPC server</span>
<span>.</span><span>usePlaintext</span><span>()</span> <span>// No TLS for simplicity</span>
<span>.</span><span>build</span><span>()</span>
<span>return</span> <span>TodoServiceGrpc</span><span>.</span><span>newBlockingStub</span><span>(</span><span>channel</span><span>)</span>
<span>}</span>
<span>}</span>
<span>package</span> <span>com.mahmud.grpcclient.config</span>
<span>import</span> <span>com.mahmud.grpcclient.TodoServiceGrpc</span>
<span>import</span> <span>io.grpc.ManagedChannel</span>
<span>import</span> <span>io.grpc.ManagedChannelBuilder</span>
<span>import</span> <span>org.springframework.context.annotation.Bean</span>
<span>import</span> <span>org.springframework.context.annotation.Configuration</span>

<span>@Configuration</span>
<span>class</span> <span>GrpcClientConfig</span> <span>{</span>

   <span>@Bean</span>
   <span>fun</span> <span>todoServiceStub</span><span>():</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span> <span>{</span>
      <span>val</span> <span>channel</span><span>:</span> <span>ManagedChannel</span> <span>=</span> <span>ManagedChannelBuilder</span>
         <span>.</span><span>forAddress</span><span>(</span><span>"localhost"</span><span>,</span> <span>9090</span><span>)</span> <span>// Connects to our gRPC server</span>
         <span>.</span><span>usePlaintext</span><span>()</span> <span>// No TLS for simplicity</span>
         <span>.</span><span>build</span><span>()</span>
      <span>return</span> <span>TodoServiceGrpc</span><span>.</span><span>newBlockingStub</span><span>(</span><span>channel</span><span>)</span>
   <span>}</span>
<span>}</span>
package com.mahmud.grpcclient.config import com.mahmud.grpcclient.TodoServiceGrpc import io.grpc.ManagedChannel import io.grpc.ManagedChannelBuilder import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class GrpcClientConfig { @Bean fun todoServiceStub(): TodoServiceGrpc.TodoServiceBlockingStub { val channel: ManagedChannel = ManagedChannelBuilder .forAddress("localhost", 9090) // Connects to our gRPC server .usePlaintext() // No TLS for simplicity .build() return TodoServiceGrpc.newBlockingStub(channel) } }

Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • We just set up a stub for server communication.

Implement the gRPC calls

The TodoClientService handles gRPC calls by communicating with the server.

TodoClientService.kt

<span>package</span> <span>com.mahmud.grpcclient.service</span>
<span>import</span> <span>com.google.protobuf.Timestamp</span>
<span>import</span> <span>com.mahmud.grpcclient.*</span>
<span>import</span> <span>org.springframework.stereotype.Service</span>
<span>import</span> <span>java.time.Instant</span>
<span>// Spring service managing gRPC client calls to the server</span>
<span>@Service</span>
<span>class</span> <span>TodoClientService</span><span>(</span><span>private</span> <span>val</span> <span>todoStub</span><span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span><span>)</span> <span>{</span>
<span>// gRPC call to create a new Todo on the server</span>
<span>fun</span> <span>createTodo</span><span>(</span><span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span>
<span>// Build gRPC request from method parameters</span>
<span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span>
<span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span>
<span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span>
<span>.</span><span>apply</span> <span>{</span>
<span>dueDate</span><span>?.</span><span>let</span> <span>{</span>
<span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span>
<span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span>
<span>.</span><span>build</span><span>())</span>
<span>}</span>
<span>}</span>
<span>.</span><span>build</span><span>()</span>
<span>// Invoke gRPC server method and return response</span>
<span>return</span> <span>todoStub</span><span>.</span><span>createTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span><span>;</span>
<span>}</span>
<span>// gRPC call to retrieve a Todo by its ID from the server</span>
<span>fun</span> <span>getTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>TodoItem</span> <span>{</span>
<span>// Construct gRPC request with Todo ID</span>
<span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span>
<span>// Fetch Todo from gRPC server and return it</span>
<span>return</span> <span>todoStub</span><span>.</span><span>getTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span>
<span>}</span>
<span>// gRPC call to list Todos, optionally showing completed ones</span>
<span>fun</span> <span>listTodos</span><span>(</span><span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>MutableList</span><span><</span><span>TodoItem</span><span>>?</span> <span>{</span>
<span>// Create gRPC request with filter parameter</span>
<span>val</span> <span>request</span> <span>=</span> <span>ListTodosRequest</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setShowCompleted</span><span>(</span><span>showCompleted</span><span>)</span>
<span>.</span><span>build</span><span>()</span>
<span>// Retrieve list from gRPC server and return as MutableList</span>
<span>return</span> <span>todoStub</span><span>.</span><span>listTodos</span><span>(</span><span>request</span><span>).</span><span>todosList</span>
<span>}</span>
<span>// gRPC call to update an existing Todo on the server</span>
<span>fun</span> <span>updateTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>,</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span>
<span>// Build gRPC request with updated Todo data</span>
<span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setId</span><span>(</span><span>id</span><span>)</span>
<span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span>
<span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span>
<span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span>
<span>.</span><span>apply</span> <span>{</span>
<span>dueDate</span><span>?.</span><span>let</span> <span>{</span>
<span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
<span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span>
<span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span>
<span>.</span><span>build</span><span>())</span>
<span>}</span>
<span>}</span>
<span>.</span><span>build</span><span>()</span>
<span>// Update Todo via gRPC server and return result</span>
<span>return</span> <span>todoStub</span><span>.</span><span>updateTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span>
<span>}</span>
<span>// gRPC call to delete a Todo by ID on the server</span>
<span>fun</span> <span>deleteTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span>
<span>// Construct gRPC request with ID to delete</span>
<span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span>
<span>// Delete Todo on gRPC server and return empty response</span>
<span>return</span> <span>todoStub</span><span>.</span><span>deleteTodo</span><span>(</span><span>request</span><span>)</span>
<span>}</span>
<span>}</span>
<span>package</span> <span>com.mahmud.grpcclient.service</span>

<span>import</span> <span>com.google.protobuf.Timestamp</span>
<span>import</span> <span>com.mahmud.grpcclient.*</span>
<span>import</span> <span>org.springframework.stereotype.Service</span>
<span>import</span> <span>java.time.Instant</span>

<span>// Spring service managing gRPC client calls to the server</span>
<span>@Service</span>
<span>class</span> <span>TodoClientService</span><span>(</span><span>private</span> <span>val</span> <span>todoStub</span><span>:</span> <span>TodoServiceGrpc</span><span>.</span><span>TodoServiceBlockingStub</span><span>)</span> <span>{</span>

   <span>// gRPC call to create a new Todo on the server</span>
   <span>fun</span> <span>createTodo</span><span>(</span><span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span>
      <span>// Build gRPC request from method parameters</span>
      <span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span>
         <span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span>
         <span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span>
         <span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span>
         <span>.</span><span>apply</span> <span>{</span>
            <span>dueDate</span><span>?.</span><span>let</span> <span>{</span>
               <span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
                  <span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span>
                  <span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span>
                  <span>.</span><span>build</span><span>())</span>
            <span>}</span>
         <span>}</span>
         <span>.</span><span>build</span><span>()</span>
      <span>// Invoke gRPC server method and return response</span>
      <span>return</span> <span>todoStub</span><span>.</span><span>createTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span><span>;</span>
   <span>}</span>

   <span>// gRPC call to retrieve a Todo by its ID from the server</span>
   <span>fun</span> <span>getTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>TodoItem</span> <span>{</span>
      <span>// Construct gRPC request with Todo ID</span>
      <span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span>
      <span>// Fetch Todo from gRPC server and return it</span>
      <span>return</span> <span>todoStub</span><span>.</span><span>getTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span>
   <span>}</span>

   <span>// gRPC call to list Todos, optionally showing completed ones</span>
   <span>fun</span> <span>listTodos</span><span>(</span><span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>MutableList</span><span><</span><span>TodoItem</span><span>>?</span> <span>{</span>
      <span>// Create gRPC request with filter parameter</span>
      <span>val</span> <span>request</span> <span>=</span> <span>ListTodosRequest</span><span>.</span><span>newBuilder</span><span>()</span>
         <span>.</span><span>setShowCompleted</span><span>(</span><span>showCompleted</span><span>)</span>
         <span>.</span><span>build</span><span>()</span>
      <span>// Retrieve list from gRPC server and return as MutableList</span>
      <span>return</span> <span>todoStub</span><span>.</span><span>listTodos</span><span>(</span><span>request</span><span>).</span><span>todosList</span>
   <span>}</span>

   <span>// gRPC call to update an existing Todo on the server</span>
   <span>fun</span> <span>updateTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>,</span> <span>title</span><span>:</span> <span>String</span><span>,</span> <span>description</span><span>:</span> <span>String</span><span>,</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span> <span>dueDate</span><span>:</span> <span>Instant</span><span>?):</span> <span>TodoItem</span><span>?</span> <span>{</span>
      <span>// Build gRPC request with updated Todo data</span>
      <span>val</span> <span>request</span> <span>=</span> <span>TodoRequest</span><span>.</span><span>newBuilder</span><span>()</span>
         <span>.</span><span>setId</span><span>(</span><span>id</span><span>)</span>
         <span>.</span><span>setTitle</span><span>(</span><span>title</span><span>)</span>
         <span>.</span><span>setDescription</span><span>(</span><span>description</span><span>)</span>
         <span>.</span><span>setCompleted</span><span>(</span><span>completed</span><span>)</span>
         <span>.</span><span>apply</span> <span>{</span>
            <span>dueDate</span><span>?.</span><span>let</span> <span>{</span>
               <span>setDueDate</span><span>(</span><span>Timestamp</span><span>.</span><span>newBuilder</span><span>()</span>
                  <span>.</span><span>setSeconds</span><span>(</span><span>it</span><span>.</span><span>epochSecond</span><span>)</span>
                  <span>.</span><span>setNanos</span><span>(</span><span>it</span><span>.</span><span>nano</span><span>)</span>
                  <span>.</span><span>build</span><span>())</span>
            <span>}</span>
         <span>}</span>
         <span>.</span><span>build</span><span>()</span>
      <span>// Update Todo via gRPC server and return result</span>
      <span>return</span> <span>todoStub</span><span>.</span><span>updateTodo</span><span>(</span><span>request</span><span>).</span><span>todo</span>
   <span>}</span>

   <span>// gRPC call to delete a Todo by ID on the server</span>
   <span>fun</span> <span>deleteTodo</span><span>(</span><span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span>
      <span>// Construct gRPC request with ID to delete</span>
      <span>val</span> <span>request</span> <span>=</span> <span>TodoIdRequest</span><span>.</span><span>newBuilder</span><span>().</span><span>setId</span><span>(</span><span>id</span><span>).</span><span>build</span><span>()</span>
      <span>// Delete Todo on gRPC server and return empty response</span>
      <span>return</span> <span>todoStub</span><span>.</span><span>deleteTodo</span><span>(</span><span>request</span><span>)</span>
   <span>}</span>
<span>}</span>
package com.mahmud.grpcclient.service import com.google.protobuf.Timestamp import com.mahmud.grpcclient.* import org.springframework.stereotype.Service import java.time.Instant // Spring service managing gRPC client calls to the server @Service class TodoClientService(private val todoStub: TodoServiceGrpc.TodoServiceBlockingStub) { // gRPC call to create a new Todo on the server fun createTodo(title: String, description: String, completed: Boolean, dueDate: Instant?): TodoItem? { // Build gRPC request from method parameters val request = TodoRequest.newBuilder() .setTitle(title) .setDescription(description) .setCompleted(completed) .apply { dueDate?.let { setDueDate(Timestamp.newBuilder() .setSeconds(it.epochSecond) .setNanos(it.nano) .build()) } } .build() // Invoke gRPC server method and return response return todoStub.createTodo(request).todo; } // gRPC call to retrieve a Todo by its ID from the server fun getTodo(id: String): TodoItem { // Construct gRPC request with Todo ID val request = TodoIdRequest.newBuilder().setId(id).build() // Fetch Todo from gRPC server and return it return todoStub.getTodo(request).todo } // gRPC call to list Todos, optionally showing completed ones fun listTodos(showCompleted: Boolean): MutableList<TodoItem>? { // Create gRPC request with filter parameter val request = ListTodosRequest.newBuilder() .setShowCompleted(showCompleted) .build() // Retrieve list from gRPC server and return as MutableList return todoStub.listTodos(request).todosList } // gRPC call to update an existing Todo on the server fun updateTodo(id: String, title: String, description: String, completed: Boolean, dueDate: Instant?): TodoItem? { // Build gRPC request with updated Todo data val request = TodoRequest.newBuilder() .setId(id) .setTitle(title) .setDescription(description) .setCompleted(completed) .apply { dueDate?.let { setDueDate(Timestamp.newBuilder() .setSeconds(it.epochSecond) .setNanos(it.nano) .build()) } } .build() // Update Todo via gRPC server and return result return todoStub.updateTodo(request).todo } // gRPC call to delete a Todo by ID on the server fun deleteTodo(id: String): Empty? { // Construct gRPC request with ID to delete val request = TodoIdRequest.newBuilder().setId(id).build() // Delete Todo on gRPC server and return empty response return todoStub.deleteTodo(request) } }

Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • @Service: Marks as a Spring service.
  • todoStub: Injected stub for calls.

DTO

Let’s define the REST response DTO for JSON serialization.

TodoItemDto.kt

<span>package</span> <span>com.mahmud.grpcclient.dto</span>
<span>import</span> <span>com.mahmud.grpcclient.TodoItem</span>
<span>data class</span> <span>TodoItemDto</span><span>(</span>
<span>val</span> <span>id</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span>
<span>val</span> <span>createdAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span>
<span>val</span> <span>updatedAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span>
<span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>// dueDate nullable</span>
<span>)</span>
<span>fun</span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>:</span> <span>TodoItem</span><span>):</span> <span>TodoItemDto</span> <span>{</span>
<span>return</span> <span>TodoItemDto</span><span>(</span>
<span>id</span> <span>=</span> <span>todoItem</span><span>.</span><span>id</span><span>,</span>
<span>title</span> <span>=</span> <span>todoItem</span><span>.</span><span>title</span><span>,</span>
<span>description</span> <span>=</span> <span>todoItem</span><span>.</span><span>description</span><span>,</span>
<span>completed</span> <span>=</span> <span>todoItem</span><span>.</span><span>completed</span><span>,</span>
<span>createdAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>createdAt</span><span>.</span><span>seconds</span><span>,</span>
<span>updatedAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>updatedAt</span><span>.</span><span>seconds</span><span>,</span>
<span>dueDate</span> <span>=</span> <span>todoItem</span><span>.</span><span>dueDate</span><span>?.</span><span>seconds</span> <span>// handle the null case</span>
<span>)</span>
<span>}</span>
<span>package</span> <span>com.mahmud.grpcclient.dto</span>

<span>import</span> <span>com.mahmud.grpcclient.TodoItem</span>

<span>data class</span> <span>TodoItemDto</span><span>(</span>
   <span>val</span> <span>id</span><span>:</span> <span>String</span><span>,</span>
   <span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span>
   <span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span>
   <span>val</span> <span>completed</span><span>:</span> <span>Boolean</span><span>,</span>
   <span>val</span> <span>createdAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span>
   <span>val</span> <span>updatedAt</span><span>:</span> <span>Long</span><span>,</span> <span>// Epoch seconds</span>
   <span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>// dueDate nullable</span>
<span>)</span>

<span>fun</span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>:</span> <span>TodoItem</span><span>):</span> <span>TodoItemDto</span> <span>{</span>
   <span>return</span> <span>TodoItemDto</span><span>(</span>
      <span>id</span> <span>=</span> <span>todoItem</span><span>.</span><span>id</span><span>,</span>
      <span>title</span> <span>=</span> <span>todoItem</span><span>.</span><span>title</span><span>,</span>
      <span>description</span> <span>=</span> <span>todoItem</span><span>.</span><span>description</span><span>,</span>
      <span>completed</span> <span>=</span> <span>todoItem</span><span>.</span><span>completed</span><span>,</span>
      <span>createdAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>createdAt</span><span>.</span><span>seconds</span><span>,</span>
      <span>updatedAt</span> <span>=</span> <span>todoItem</span><span>.</span><span>updatedAt</span><span>.</span><span>seconds</span><span>,</span>
      <span>dueDate</span> <span>=</span> <span>todoItem</span><span>.</span><span>dueDate</span><span>?.</span><span>seconds</span> <span>// handle the null case</span>
   <span>)</span>
<span>}</span>
package com.mahmud.grpcclient.dto import com.mahmud.grpcclient.TodoItem data class TodoItemDto( val id: String, val title: String, val description: String, val completed: Boolean, val createdAt: Long, // Epoch seconds val updatedAt: Long, // Epoch seconds val dueDate: Long? // dueDate nullable ) fun mapToTodoItemDto(todoItem: TodoItem): TodoItemDto { return TodoItemDto( id = todoItem.id, title = todoItem.title, description = todoItem.description, completed = todoItem.completed, createdAt = todoItem.createdAt.seconds, updatedAt = todoItem.updatedAt.seconds, dueDate = todoItem.dueDate?.seconds // handle the null case ) }

Enter fullscreen mode Exit fullscreen mode

Define the DTO for Todo Request:

TodoRequestDto.kt

<span>package</span> <span>com.mahmud.grpcclient.dto</span>
<span>data class</span> <span>TodoRequestDto</span><span>(</span>
<span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span>
<span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span>
<span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>=</span> <span>null</span> <span>// Epoch seconds </span>
<span>)</span>
<span>package</span> <span>com.mahmud.grpcclient.dto</span>

<span>data class</span> <span>TodoRequestDto</span><span>(</span>
   <span>val</span> <span>title</span><span>:</span> <span>String</span><span>,</span>
   <span>val</span> <span>description</span><span>:</span> <span>String</span><span>,</span>
   <span>val</span> <span>completed</span><span>:</span> <span>Boolean</span> <span>=</span> <span>false</span><span>,</span>
   <span>val</span> <span>dueDate</span><span>:</span> <span>Long</span><span>?</span> <span>=</span> <span>null</span> <span>// Epoch seconds </span>
<span>)</span>
package com.mahmud.grpcclient.dto data class TodoRequestDto( val title: String, val description: String, val completed: Boolean = false, val dueDate: Long? = null // Epoch seconds )

Enter fullscreen mode Exit fullscreen mode

Expose REST endpoints

TodoController.kt

REST controller bridging HTTP requests to gRPC calls with detailed comments.

<span>package</span> <span>com.mahmud.grpcclient.controller</span>
<span>import</span> <span>com.mahmud.grpcclient.Empty</span>
<span>import</span> <span>com.mahmud.grpcclient.service.TodoClientService</span>
<span>import</span> <span>com.mahmud.grpcclient.TodoItem</span>
<span>import</span> <span>com.mahmud.grpcclient.dto.TodoItemDto</span>
<span>import</span> <span>com.mahmud.grpcclient.dto.TodoRequestDto</span>
<span>import</span> <span>com.mahmud.grpcclient.dto.mapToTodoItemDto</span>
<span>import</span> <span>org.springframework.web.bind.annotation.*</span>
<span>import</span> <span>reactor.core.publisher.Flux</span>
<span>import</span> <span>reactor.core.publisher.Mono</span>
<span>import</span> <span>java.time.Instant</span>
<span>// REST controller exposing Todo endpoints over HTTP</span>
<span>@RestController</span>
<span>@RequestMapping</span><span>(</span><span>"/api/todos"</span><span>)</span>
<span>class</span> <span>TodoController</span><span>(</span><span>private</span> <span>val</span> <span>todoService</span><span>:</span> <span>TodoClientService</span><span>)</span> <span>{</span>
<span>// POST endpoint to create a new Todo via gRPC</span>
<span>@PostMapping</span>
<span>fun</span> <span>createTodo</span><span>(</span>
<span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span>
<span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
<span>// Delegate to gRPC service to create Todo</span>
<span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>createTodo</span><span>(</span>
<span>request</span><span>.</span><span>title</span><span>,</span>
<span>request</span><span>.</span><span>description</span><span>,</span>
<span>request</span><span>.</span><span>completed</span><span>,</span>
<span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span>
<span>)</span>
<span>// Wrap gRPC response in reactive Mono for REST</span>
<span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span>
<span>}</span>
<span>// GET endpoint to retrieve a Todo by ID via gRPC</span>
<span>@GetMapping</span><span>(</span><span>"/{id}"</span><span>)</span>
<span>fun</span> <span>getTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
<span>// Fetch Todo from gRPC service and map to DTO</span>
<span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>mapToTodoItemDto</span><span>(</span><span>todoService</span><span>.</span><span>getTodo</span><span>(</span><span>id</span><span>)))</span>
<span>}</span>
<span>// GET endpoint to list Todos, with optional completion filter</span>
<span>@GetMapping</span>
<span>fun</span> <span>listTodos</span><span>(</span><span>@RequestParam</span><span>(</span><span>defaultValue</span> <span>=</span> <span>"true"</span><span>)</span> <span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
<span>// Retrieve list from gRPC service</span>
<span>val</span> <span>todoList</span> <span>=</span> <span>todoService</span><span>.</span><span>listTodos</span><span>(</span><span>showCompleted</span><span>)</span>
<span>// Convert gRPC list to reactive Flux of DTOs</span>
<span>return</span> <span>todoList</span><span>?.</span><span>let</span> <span>{</span> <span>Flux</span><span>.</span><span>fromIterable</span><span>(</span><span>it</span><span>).</span><span>map</span> <span>{</span> <span>todoItem</span> <span>-></span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>)</span> <span>}</span> <span>}</span> <span>?:</span> <span>Flux</span><span>.</span><span>empty</span><span>()</span>
<span>}</span>
<span>// PUT endpoint to update a Todo via gRPC</span>
<span>@PutMapping</span><span>(</span><span>"/{id}"</span><span>)</span>
<span>fun</span> <span>updateTodo</span><span>(</span>
<span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>,</span>
<span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span>
<span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
<span>// Update Todo using gRPC service</span>
<span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>updateTodo</span><span>(</span>
<span>id</span><span>,</span>
<span>request</span><span>.</span><span>title</span><span>,</span>
<span>request</span><span>.</span><span>description</span><span>,</span>
<span>request</span><span>.</span><span>completed</span><span>,</span>
<span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span>
<span>)</span>
<span>// Return updated Todo as reactive Mono</span>
<span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span>
<span>}</span>
<span>// DELETE endpoint to remove a Todo via gRPC</span>
<span>@DeleteMapping</span><span>(</span><span>"/{id}"</span><span>)</span>
<span>fun</span> <span>deleteTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span>
<span>// Call gRPC service to delete Todo and return empty response</span>
<span>return</span> <span>todoService</span><span>.</span><span>deleteTodo</span><span>(</span><span>id</span><span>)</span>
<span>}</span>
<span>}</span>
<span>package</span> <span>com.mahmud.grpcclient.controller</span>

<span>import</span> <span>com.mahmud.grpcclient.Empty</span>
<span>import</span> <span>com.mahmud.grpcclient.service.TodoClientService</span>
<span>import</span> <span>com.mahmud.grpcclient.TodoItem</span>
<span>import</span> <span>com.mahmud.grpcclient.dto.TodoItemDto</span>
<span>import</span> <span>com.mahmud.grpcclient.dto.TodoRequestDto</span>
<span>import</span> <span>com.mahmud.grpcclient.dto.mapToTodoItemDto</span>
<span>import</span> <span>org.springframework.web.bind.annotation.*</span>
<span>import</span> <span>reactor.core.publisher.Flux</span>
<span>import</span> <span>reactor.core.publisher.Mono</span>
<span>import</span> <span>java.time.Instant</span>

<span>// REST controller exposing Todo endpoints over HTTP</span>
<span>@RestController</span>
<span>@RequestMapping</span><span>(</span><span>"/api/todos"</span><span>)</span>
<span>class</span> <span>TodoController</span><span>(</span><span>private</span> <span>val</span> <span>todoService</span><span>:</span> <span>TodoClientService</span><span>)</span> <span>{</span>

   <span>// POST endpoint to create a new Todo via gRPC</span>
   <span>@PostMapping</span>
   <span>fun</span> <span>createTodo</span><span>(</span>
      <span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span>
   <span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
      <span>// Delegate to gRPC service to create Todo</span>
      <span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>createTodo</span><span>(</span>
         <span>request</span><span>.</span><span>title</span><span>,</span>
         <span>request</span><span>.</span><span>description</span><span>,</span>
         <span>request</span><span>.</span><span>completed</span><span>,</span>
         <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span>
      <span>)</span>
      <span>// Wrap gRPC response in reactive Mono for REST</span>
      <span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span>
   <span>}</span>

   <span>// GET endpoint to retrieve a Todo by ID via gRPC</span>
   <span>@GetMapping</span><span>(</span><span>"/{id}"</span><span>)</span>
   <span>fun</span> <span>getTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
      <span>// Fetch Todo from gRPC service and map to DTO</span>
      <span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>mapToTodoItemDto</span><span>(</span><span>todoService</span><span>.</span><span>getTodo</span><span>(</span><span>id</span><span>)))</span>
   <span>}</span>

   <span>// GET endpoint to list Todos, with optional completion filter</span>
   <span>@GetMapping</span>
   <span>fun</span> <span>listTodos</span><span>(</span><span>@RequestParam</span><span>(</span><span>defaultValue</span> <span>=</span> <span>"true"</span><span>)</span> <span>showCompleted</span><span>:</span> <span>Boolean</span><span>):</span> <span>Flux</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
      <span>// Retrieve list from gRPC service</span>
      <span>val</span> <span>todoList</span> <span>=</span> <span>todoService</span><span>.</span><span>listTodos</span><span>(</span><span>showCompleted</span><span>)</span>
      <span>// Convert gRPC list to reactive Flux of DTOs</span>
      <span>return</span> <span>todoList</span><span>?.</span><span>let</span> <span>{</span> <span>Flux</span><span>.</span><span>fromIterable</span><span>(</span><span>it</span><span>).</span><span>map</span> <span>{</span> <span>todoItem</span> <span>-></span> <span>mapToTodoItemDto</span><span>(</span><span>todoItem</span><span>)</span> <span>}</span> <span>}</span> <span>?:</span> <span>Flux</span><span>.</span><span>empty</span><span>()</span>
   <span>}</span>

   <span>// PUT endpoint to update a Todo via gRPC</span>
   <span>@PutMapping</span><span>(</span><span>"/{id}"</span><span>)</span>
   <span>fun</span> <span>updateTodo</span><span>(</span>
      <span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>,</span>
      <span>@RequestBody</span> <span>request</span><span>:</span> <span>TodoRequestDto</span>
   <span>):</span> <span>Mono</span><span><</span><span>TodoItemDto</span><span>></span> <span>{</span>
      <span>// Update Todo using gRPC service</span>
      <span>val</span> <span>todo</span> <span>=</span> <span>todoService</span><span>.</span><span>updateTodo</span><span>(</span>
         <span>id</span><span>,</span>
         <span>request</span><span>.</span><span>title</span><span>,</span>
         <span>request</span><span>.</span><span>description</span><span>,</span>
         <span>request</span><span>.</span><span>completed</span><span>,</span>
         <span>request</span><span>.</span><span>dueDate</span><span>?.</span><span>let</span> <span>{</span> <span>Instant</span><span>.</span><span>ofEpochSecond</span><span>(</span><span>it</span><span>)</span> <span>}</span>
      <span>)</span>
      <span>// Return updated Todo as reactive Mono</span>
      <span>return</span> <span>Mono</span><span>.</span><span>justOrEmpty</span><span>(</span><span>todo</span><span>?.</span><span>let</span> <span>{</span> <span>mapToTodoItemDto</span><span>(</span><span>it</span><span>)</span> <span>})</span>
   <span>}</span>

   <span>// DELETE endpoint to remove a Todo via gRPC</span>
   <span>@DeleteMapping</span><span>(</span><span>"/{id}"</span><span>)</span>
   <span>fun</span> <span>deleteTodo</span><span>(</span><span>@PathVariable</span> <span>id</span><span>:</span> <span>String</span><span>):</span> <span>Empty</span><span>?</span> <span>{</span>
      <span>// Call gRPC service to delete Todo and return empty response</span>
      <span>return</span> <span>todoService</span><span>.</span><span>deleteTodo</span><span>(</span><span>id</span><span>)</span>
   <span>}</span>
<span>}</span>
package com.mahmud.grpcclient.controller import com.mahmud.grpcclient.Empty import com.mahmud.grpcclient.service.TodoClientService import com.mahmud.grpcclient.TodoItem import com.mahmud.grpcclient.dto.TodoItemDto import com.mahmud.grpcclient.dto.TodoRequestDto import com.mahmud.grpcclient.dto.mapToTodoItemDto import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux import reactor.core.publisher.Mono import java.time.Instant // REST controller exposing Todo endpoints over HTTP @RestController @RequestMapping("/api/todos") class TodoController(private val todoService: TodoClientService) { // POST endpoint to create a new Todo via gRPC @PostMapping fun createTodo( @RequestBody request: TodoRequestDto ): Mono<TodoItemDto> { // Delegate to gRPC service to create Todo val todo = todoService.createTodo( request.title, request.description, request.completed, request.dueDate?.let { Instant.ofEpochSecond(it) } ) // Wrap gRPC response in reactive Mono for REST return Mono.justOrEmpty(todo?.let { mapToTodoItemDto(it) }) } // GET endpoint to retrieve a Todo by ID via gRPC @GetMapping("/{id}") fun getTodo(@PathVariable id: String): Mono<TodoItemDto> { // Fetch Todo from gRPC service and map to DTO return Mono.justOrEmpty(mapToTodoItemDto(todoService.getTodo(id))) } // GET endpoint to list Todos, with optional completion filter @GetMapping fun listTodos(@RequestParam(defaultValue = "true") showCompleted: Boolean): Flux<TodoItemDto> { // Retrieve list from gRPC service val todoList = todoService.listTodos(showCompleted) // Convert gRPC list to reactive Flux of DTOs return todoList?.let { Flux.fromIterable(it).map { todoItem -> mapToTodoItemDto(todoItem) } } ?: Flux.empty() } // PUT endpoint to update a Todo via gRPC @PutMapping("/{id}") fun updateTodo( @PathVariable id: String, @RequestBody request: TodoRequestDto ): Mono<TodoItemDto> { // Update Todo using gRPC service val todo = todoService.updateTodo( id, request.title, request.description, request.completed, request.dueDate?.let { Instant.ofEpochSecond(it) } ) // Return updated Todo as reactive Mono return Mono.justOrEmpty(todo?.let { mapToTodoItemDto(it) }) } // DELETE endpoint to remove a Todo via gRPC @DeleteMapping("/{id}") fun deleteTodo(@PathVariable id: String): Empty? { // Call gRPC service to delete Todo and return empty response return todoService.deleteTodo(id) } }

Enter fullscreen mode Exit fullscreen mode

Breakdown:

  • @RestController: Exposes REST endpoints.
  • Mono/Flux: Handles reactive output.

Step 5: Run the Client

./gradlew bootRun
./gradlew bootRun
./gradlew bootRun

Enter fullscreen mode Exit fullscreen mode

LEt’s test the gRPC Client using curl

The client exposes REST endpoints at http://localhost:8080/api/todos, mapping to the gRPC server’s functionality.

1. Create Todo (POST)

Creates a new Todo via the REST API.

curl <span>-X</span> POST http://localhost:8080/api/todos <span>\</span>
<span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span>
<span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "dueDate": 1735689600 }'</span>
curl <span>-X</span> POST http://localhost:8080/api/todos <span>\</span>
  <span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span>
  <span>-d</span> <span>'{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "dueDate": 1735689600 }'</span>
curl -X POST http://localhost:8080/api/todos \ -H "Content-Type: application/json" \ -d '{ "title": "Buy groceries", "description": "Milk, bread, eggs", "completed": false, "dueDate": 1735689600 }'

Enter fullscreen mode Exit fullscreen mode

  • Payload: TodoRequestDto with dueDate as epoch seconds (Dec 31, 2025, 00:00:00 UTC).
  • Response: JSON TodoItemDto with the created Todo.

2. Get Todo (GET)

Retrieves a Todo by ID (replace <todo-id> with an actual ID).

curl <span>-X</span> GET http://localhost:8080/api/todos/<todo-id>
curl <span>-X</span> GET http://localhost:8080/api/todos/<todo-id>
curl -X GET http://localhost:8080/api/todos/<todo-id>

Enter fullscreen mode Exit fullscreen mode

  • Path: Uses the Todo’s ID.
  • Response: JSON TodoItemDto with the Todo’s details.

3. List Todos (GET)

Lists all Todos, with an optional showCompleted filter.

curl <span>-X</span> GET <span>"http://localhost:8080/api/todos?showCompleted=true"</span>
curl <span>-X</span> GET <span>"http://localhost:8080/api/todos?showCompleted=true"</span>
curl -X GET "http://localhost:8080/api/todos?showCompleted=true"

Enter fullscreen mode Exit fullscreen mode

  • Query param: showCompleted=true includes completed Todos; set to false to exclude them.
  • Response: JSON array of TodoItemDtos.

4. Update Todo (PUT)

Updates an existing Todo (replace <todo-id> with an actual ID).

curl <span>-X</span> PUT http://localhost:8080/api/todos/<todo-id> <span>\</span>
<span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span>
<span>-d</span> <span>'{ "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "dueDate": 1735776000 }'</span>
curl <span>-X</span> PUT http://localhost:8080/api/todos/<todo-id> <span>\</span>
  <span>-H</span> <span>"Content-Type: application/json"</span> <span>\</span>
  <span>-d</span> <span>'{ "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "dueDate": 1735776000 }'</span>
curl -X PUT http://localhost:8080/api/todos/<todo-id> \ -H "Content-Type: application/json" \ -d '{ "title": "Buy groceries updated", "description": "Milk, bread, eggs, cheese", "completed": true, "dueDate": 1735776000 }'

Enter fullscreen mode Exit fullscreen mode

  • Payload: TodoRequestDto with updated fields, dueDate as epoch seconds (Jan 1, 2026, 00:00:00 UTC).
  • Response: JSON TodoItemDto with the updated Todo.

5. Delete Todo (DELETE)

Deletes a Todo by ID (replace <todo-id> with an actual ID).

curl <span>-X</span> DELETE http://localhost:8080/api/todos/<todo-id>
curl <span>-X</span> DELETE http://localhost:8080/api/todos/<todo-id>
curl -X DELETE http://localhost:8080/api/todos/<todo-id>

Enter fullscreen mode Exit fullscreen mode

  • Path: Uses the Todo’s ID.
  • Response: Empty response (HTTP 200 OK).

Conclusion

This guide provides a complete implementation of a Todo application using gRPC with Spring Boot 3 and Spring gRPC. By building the server and client sequentially, we’ve demonstrated gRPC’s capabilities within a Spring ecosystem, leveraging Kotlin and MongoDB.

Key takeaways:

  • gRPC’s performance and typing advantages make it ideal for service-to-service communication.
  • Spring gRPC simplifies gRPC adoption in Spring Boot, despite its experimental status.
  • Reactive programming enhances scalability.

For production, consider TLS, full reactive flows, and streaming features. As Spring gRPC matures, it will solidify its place in modern architectures.

References

原文链接:Implementing gRPC with Kotlin & Spring Boot 3: A Comprehensive Guide

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
Judge each day not by the harvest you reap but by the seeds you plant.
不要问自己收获了多少果实,而是要问自己今天播种了多少种子
评论 抢沙发

请登录后发表评论

    暂无评论内容