SaFi Bank Space : How to implement gRPC in Micronaut Kotlin

(blue star) Instructions

Step by step implement gRPC for Micronaut Kotlin:

1. Add gRPC and protobuf version in gradle.properties

file: gradle.properties

...
protobufVersion=3.21.11
grpcProtobufVersion=1.51.1
grpcKotlinVersion=1.3.0

2. Add gradle plugin and version definition in build.gradle.kts

file: build.gradle.kts

import com.google.protobuf.gradle.id

plugins {
  ...
  id("com.google.protobuf") version "0.9.1"
}

val protobufVersion = project.properties["protobufVersion"]
val grpcProtobufVersion = project.properties["grpcProtobufVersion"]
val grpcKotlinVersion = project.properties["grpcKotlinVersion"]

3. Add gRPC and protobuf dependencies

file: build.gradle.kts

dependencies {
    ...
    implementation("io.grpc:grpc-kotlin-stub:$grpcKotlinVersion")
    compileOnly("io.grpc:grpc-stub:$grpcProtobufVersion")
    implementation("io.grpc:grpc-protobuf:$grpcProtobufVersion")
    implementation("com.google.protobuf:protobuf-kotlin:$protobufVersion")
    implementation("io.micronaut.grpc:micronaut-grpc-server-runtime:3.4.0")
    protobuf("com.google.protobuf:protobuf-java:$protobufVersion")
}

4. Add source set in build.gradle.kts

file: build.gradle.kts

sourceSets {
    main {
        java {
            srcDirs("build/generated/source/proto/main/grpc")
            srcDirs("build/generated/source/proto/main/grpckt")
            srcDirs("build/generated/source/proto/main/java")
        }
    }
}

5. Add protobuf task in build.gradle.kts

file: build.gradle.kts

protobuf {
    protoc { artifact = "com.google.protobuf:protoc:$protobufVersion" }
    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:$grpcProtobufVersion"
        }
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpcKotlinVersion:jdk8@jar"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
        }
    }
}

6. Create protobuf file in folder src/main/proto/

file: src/main/proto/CardManagerV1.proto

syntax = "proto3";


option java_multiple_files = true;
option java_package = "cardManagerV1";
option java_outer_classname = "CardManagerV1Proto";
option objc_class_prefix = "CMS";

package cardmanagerv1;

service CardManagerV1 {
  rpc GetCardDetails (GetCardDetailsMessageV1) returns (CardDetailsMessageV1) {}
}

message CardDetailsMessageV1 {
  string cardId = 1;
  string accountNo = 2;
  string accountType = 3;
  string customerId = 4;
  string cardType = 5;
  string cardPicture = 6;
  optional bool namePrinted = 7;
  optional bool dynamicCVV = 8;
  optional bool internationalBlockingFlag = 9;
  string embossName = 10;
  string maskingCardNo = 11;
  string expiration = 12;
  string cardStatus = 13;
  optional int32 orderingNumber = 14;
  string key = 15;
  optional string createdAt = 16;
  optional string updatedAt = 17;
}
message GetCardDetailsMessageV1 {
  string cardId = 1;
  bool isSyncTracking = 2;
}

Or you can split the definition by creating the others .proto file to define each message model:

file: src/main/proto/messages/CardDetailsMessageV1.proto

syntax = "proto3";

option java_package = "cardManagerV1";
option java_outer_classname = "CardDetailsMessageV1Proto";
option objc_class_prefix = "CMS";

message CardDetailsMessageV1 {
  string cardId = 1;
  string accountNo = 2;
  string accountType = 3;
  string customerId = 4;
  string cardType = 5;
  string cardPicture = 6;
  optional bool namePrinted = 7;
  optional bool dynamicCVV = 8;
  optional bool internationalBlockingFlag = 9;
  string embossName = 10;
  string maskingCardNo = 11;
  string expiration = 12;
  string cardStatus = 13;
  optional int32 orderingNumber = 14;
  string key = 15;
  optional string createdAt = 16;
  optional string updatedAt = 17;
}

file: src/main/proto/messages/GetCardDetailsMessageV1.proto

syntax = "proto3";

option java_package = "cardManagerV1";
option java_outer_classname = "GetCardDetailsMessageV1Proto";
option objc_class_prefix = "CMS";

message GetCardDetailsMessageV1 {
  string cardId = 1;
  bool isSyncTracking = 2;
}

file: src/main/proto/CardManagerV1.proto

syntax = "proto3";


option java_multiple_files = true;
option java_package = "cardManagerV1";
option java_outer_classname = "CardManagerV1Proto";
option objc_class_prefix = "CMS";

package cardmanagerv1;

import "messages/CardDetailsMessageV1.proto";
import "messages/GetCardDetailsMessageV1.proto";

service CardManagerV1 {
  rpc GetCardDetails (GetCardDetailsMessageV1) returns (CardDetailsMessageV1) {}
}

More details: https://developers.google.com/protocol-buffers/docs/proto3

7. Generate protobuf class

To generate protobuf to be java/kotlin class you can run ./gradlew generateProto or just build the project. Then the generated code will be stored in build/generated/source/proto/*

8. Create gRPC resolver class

file: src/main/kotlin/ph.safibank.cardmanager/grpc/CardManagerGrpcV1.kt

package ph.safibank.cardmanager.grpc

import cardManagerV1.CardDetailsMessageV1Proto
import cardManagerV1.CardManagerV1GrpcKt
import cardManagerV1.GetCardDetailsMessageV1Proto
import jakarta.inject.Singleton
import ph.safibank.cardmanager.grpc.mapper.toCardDetailsMessageV1
import ph.safibank.cardmanager.service.CardService

@Singleton
class CardManagerGrpcV1(private val cardService: CardService) : CardManagerV1GrpcKt.CardManagerV1CoroutineImplBase() {
    override suspend fun getCardDetails(request: GetCardDetailsMessageV1Proto.GetCardDetailsMessageV1): CardDetailsMessageV1Proto.CardDetailsMessageV1 {
        val result = cardService.getCardByCardId(request.cardId, request.isSyncTracking)
        return result.toCardDetailsMessageV1()
    }
}

In above example, class CardManagerGrpcV1 has method getCardDetails that return type is CardDetailsMessageV1Proto.CardDetailsMessageV1. Basically the generated proto will create the builder class to generate the message object, then in this example I split it to mapper function for resuseability.

file: src/main/kotlin/ph.safibank.cardmanager/grpc/mapper/ToCardDetailsMessageV1.kt

package ph.safibank.cardmanager.grpc.mapper

import cardManagerV1.CardDetailsMessageV1Proto
import ph.safibank.cardmanager.model.Card

fun Card.toCardDetailsMessageV1(): CardDetailsMessageV1Proto.CardDetailsMessageV1 {
    val cardDetailsBuilder = CardDetailsMessageV1Proto.CardDetailsMessageV1.newBuilder()
    cardDetailsBuilder
        .setCardId(this.cardId)
        .setAccountNo(this.accountNo)
        .setCustomerId(this.customerId.toString())
        .setCardType(this.cardType)
        .setCardPicture(this.cardPicture)
        .setEmbossName(this.embossName)
        .setMaskingCardNo(this.maskingCardNo)
        .setExpiration(this.expiration)
        .setCardStatus(this.cardStatus)
        .setKey(this.key)

    if (this.namePrinted != null) {
        cardDetailsBuilder.namePrinted = this.namePrinted!!
    }
    if (this.dynamicCVV != null) {
        cardDetailsBuilder.dynamicCVV = this.dynamicCVV!!
    }
    if (this.internationalBlockingFlag != null) {
        cardDetailsBuilder.internationalBlockingFlag = this.internationalBlockingFlag!!
    }
    if (this.createdAt != null) {
        cardDetailsBuilder.createdAt = this.createdAt!!.toString()
    }
    if (this.updatedAt != null) {
        cardDetailsBuilder.updatedAt = this.updatedAt!!.toString()
    }
    if (this.orderingNumber != null) {
        cardDetailsBuilder.orderingNumber = this.orderingNumber!!
    }

    return cardDetailsBuilder.build()
}

9. Update application.yml

To prevent port conflict I suggest to change the port of gRPC server by add following properties to application.yml

file: src/main/resources/application.yml

...
grpc:
  server:
    port: 8081

10. Done, start the application

See example: https://github.com/SafiBank/SaFiMono/tree/SM-XXX/PoC-grpc-micronaut

https://micronaut-projects.github.io/micronaut-grpc/3.4.0/guide/index.html#server

https://developers.google.com/protocol-buffers/docs/proto3