Building a Firebase Notification service with gRPC and Ktor (Part 1)
Welcome to my tutorial series on building a gRPC service with Firebase and Ktor! Building fast efficient backends have always fascinated me and since delving into this world, I thought I would write about some of the findings I’ve done. Hopefully you gain as much as I did from this experience and find this useful.
In this series we will delve into how we can leverage the power of Firebase, gRPC and Ktor by building two internal services that will work together to deliver App notifications to end users on Android. While not shown here, this approach will work on iOS as well.
A quick overview of both our server and client application and our mobile app are as follows:
- TrainInfo Transport API – This is an API that communicates with the London Underground Service and pulls the current status of the lines in the underground
- Notification Service – This is an internal API that receives notification requests via gRPC and then actions them by sending a Firebase Cloud Message to Firebase.
- TrainInfo App – Our Android app that receives these notifications once the service has sent them
Overview of articles:
- Part 1 (This article) will focus on setting up and configuring Gradle with the gRPC depenencies for both projects as well as the Protobuf files. We will also configure the client here.
- Part 2 (Coming Soon) will focus on setting up our methods both on the TrainInfo API and on the Firebase Notification Service. Server setup is also here
- Part 3 will focus on highlighting our Android app and making sure that it’s correctly configured to receive notifications, and test this out in the end. This should also work for iOS.
- Part 4: Bonus article will finally close off with writing a health service check for Ktor if you have internal systems capable of doing service checks via gRPC. I will demonstrate this with using Consul, as it can be run on a local machine quite easily.
Just a few disclaimers below, however if you would like to skip straight to the code, thats below 🙂
Ktor – If you’ve worked with backend frameworks, Ktor is hopefully no stranger to you. It’s a Kotlin-based web framework that’s highly modular. This allows for a more direct feature implementation—adding only what is necessary for the task at hand. For our tutorial here, we’re only adding the bare minimum required to bring the server up and running while keeping it small. To check out more of Ktor, go here
gRPC – As mentioned, we also use a technology called gRPC. This is a real-time communications protocol developed by Google. It’s more focused on efficient data communications and efficiency better than JSON and XML, especially for internal communications between backends. Its got support for multiple programming languages including C++, Python and Go. For our demo here we will leave out transport security and use plaintext communication, however production, definitely add some security 🙂 To read up more on gRPC, check it out here
Protocol Buffers (Protobuf) – Developed by Google, this is a highly efficient serialization format that written in a language neutral way. By writing your RPC calls in this formats, it can then be compiled down into a variety of languages (basically what gRPC supports) as a stub. These stubs are what we’re going to use to send and receive messages. To read up more on Protocol Buffers, check out this link
Now that we have those prerequisites out the way, lets begin!
So since we’re working with two projects (client and server) , we’ll reference each part in kind. The TrainInfo API in this scenario is going to be the client, and the Notification Service is the server.
It may seem redundant given the code base below to have a client and a server instead of collapsing everything in this demo in the server directly i.e, just getting the notification info from REST and sending it directly to Firebase, however this doesn’t scale. I’m trying to show a scenario where you have a couple of different services in your environment and any of them would send event-based notifications to your users depending on some pre-defined event trigger. The Firebase service is self-contained and can be scaled across your environment.
Getting Started with Configuration
For both client and server, I’ve created both the starting projects for the client and server that just implement the dependencies, you can find them here:
The dependency setup in both projects make up the core gRPC dependencies which will generate the stubs required, in Kotlin at compile time. I’ve also configured the “proto” directory, just below the kotlin directory so that it’s marked as a source set directory for the library to find the required files. This directory will contain all your protobuf files in future, so if you want to expand your services further, keep all those files here 🙂
We’re going to start by creating the Notification Protobuf definition file. As mentioned before, the Protobuf file is used to define what will be sent by the client and how the server will receive the messages. Create a new file here, called “notification.proto”, and paste the below code inside the file:
syntax = "proto3";
package dev.vusi.trainline.notification;
message NotificationRequest {
string userid = 1; //first argument in the method definition
string title = 2; //second, argument, etc
string message = 3;
string imageurl = 4;
}
// we also need to define what response to expect here, in this case a successResponse.
message NotificationResponse {
boolean isSuccess = 1;
}
// we define the method the client and server will use here.
service NotificationService {
rpc SendNotification(NotificationRequest) returns (NotificationResponse){}
}
Once we define this file, let’s rebuild the project. This will trigger the stub generation step for our Protobuf files to be generated.
Once the build is done, we can start defining our client operations, starting with an interface we can use to access the gRPC service. You can create these files in a structure of your choice:
interface NotifyService {
fun startService()
fun shutDown()
suspend fun sendNotification(userId: String, title: String, message: String)
}
Next, we need to define the class that implements our interface and handles the operations internally:
class NotifyServiceImpl : NotifyService {
private lateinit var stub: NotificationServiceGrpcKt.NotificationServiceCoroutineStub
private var managedChannel: ManagedChannel? = null
override fun startService() {
managedChannel = createManagedChannelBuilder()
}
override fun shutDown() {
managedChannel?.shutdown()?.awaitTermination(5, TimeUnit.SECONDS)
}
override suspend fun sendNotification(userId: String, title: String, message: String, override suspend fun sendNotification(userId: String, title: String, message: String, imageUrl: String): Boolean {
val notificationRequest = NotificationRequest.newBuilder()
.setUserid(userId)
.setTitle(title)
.setMessage(message)
.setImageUrl(imageUrl)
.build()
val response = stub.sendNotification(notificationRequest)
return response.isSuccess
}
private fun createManagedChannelBuilder(): ManagedChannel {
return ManagedChannelBuilder
.forTarget("0.0.0.0:5000")
.usePlaintext()
.build()
}
}
Here we’ve defined the following:
Startup/Shutdown Process – Ktor has application process monitoring so we can start our gRPC client this way. For example when Ktor starts up/ shuts down, the lifecycle of these evens will also be propagated to our gRPC client here that will do the same.
A ManagedChannel – Server connection management. This maintains the connection to our defined gRPC server ip address/domain, tear it down when it’s no longer needed or reconnects to the it if required. We can also specify the server domain name. If you want to really have some fun and are using a service mesh configuration (such as Consul or Kubernetes), you would specify the service mesh gRPC server IP/domain and port here. We’ll explore service mesh connectivity in a later post.
Plain Text – The client is configured to use plain text connectivity. While this is great in our dev environment, for production, I strongly advise the default to be transport security with the required security configs. We could explore this in another post.
Notification Send Request – The stub that was generated by our Protobuf file also contains the SendNotification command – this is the actual RPC call that will be sent to our server.
Next, we need to setup how the client gets started. The scenario here is that we’re running two request/response services, one RPC and one that will consume REST requests. We can’t have them running concurrently in the same thread. So what do we do? Use coroutines of course! Specifically, we will run gRPC in its own coroutine. That way it can coexist with Ktor, and both services can process their own requests and responses. We will still have gRPC shutdown when the Ktor server shutdown as part of the application lifecycle events process.
Open up your Application.kt and define the following:
fun Application.module() {
val appCoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val notifyService: NotifyService = NotifyServiceImpl()
configureRouting(notifyService)
environment.monitor.subscribe(ApplicationStarted) {
appCoroutineScope.launch {
notifyService.startService()
}
}
environment.monitor.subscribe(ApplicationStopping) {
appCoroutineScope.launch {
notifyService.shutDown()
}
appCoroutineScope.cancel("Shut Down of ktor server")
}
}
As mentioned above, we can monitor the Ktor lifecycle, and start/stop our gRPC service based on these lifecycle events. I’ve also defined the Coroutine Scope where our gRPC service will run and cancel that scope when the Ktor server is being shut down.
The only thing left here that we can define pretty quickly is REST endpoint that will let us test our notification messages. I’ve defined this below:
fun Application.configureRouting(notifyService: NotifyService) {
routing {
post("/sendNotification") {
val notificationRequest = call.receive<NotificationRequest>()
val handler = ClientRequestHandler(notifyService)
val response = handler.sendRequestedNotificationDueToSomeTrigger(request = notificationRequest)
when (response.isSuccess) {
true -> call.respond(HttpStatusCode.OK, response)
else -> call.respond(HttpStatusCode.InternalServerError, response)
}
}
}
}
This endpoint will accept notification requests via REST, send the message via gRPC, and let us know via REST if the transaction was successful.
Lastly, we have our ClientRequestHandler class that shows sending the notification in action:
class ClientRequestHandler(private val notifyService: NotifyService) {
suspend fun sendRequestedNotificationDueToSomeTrigger(request: NotificationRequest): NotificationResponse {
val response = notifyService.sendNotification(
userId = request.userId,
title = request.title,
message = request.message,
imageUrl = request.imageUrl
)
return NotificationResponse(isSuccess = response)
}
}
For the sake of keeping things simple, I created an instance the ClientRequestHandler class (this would be some repository in your code) directly in the routing module, but in production code of course this would be a much cleaner implementation and in line with your code structure.
That should cover our client setup. Lastly, run your code and ensure that both Ktor and the gRPC client has started!
Thanks for reading! In Part 2 we will setup our server.