Reader small image

You're reading from  Kotlin Design Patterns and Best Practices - Second Edition

Product typeBook
Published inJan 2022
Reading LevelBeginner
PublisherPackt
ISBN-139781801815727
Edition2nd Edition
Languages
Right arrow
Author (1)
Alexey Soshin
Alexey Soshin
author image
Alexey Soshin

Alexey Soshin is a software architect with 15 years of experience in the industry. He started exploring Kotlin when Kotlin was still in beta, and since then has been a big enthusiast of the language. He's a conference speaker, published writer, and the author of a video course titled Pragmatic System Design.
Read more about Alexey Soshin

Right arrow

Chapter 10: Concurrent Microservices with Ktor

In the previous chapter, we explored how we should write idiomatic Kotlin code that will be readable and maintainable, as well as performant.

In this chapter, we'll put the skills we've learned so far to use by building a microservice using the Ktor framework. We also want this microservice to be reactive and to be as close to real life as possible. For that, we'll use the Ktor framework, the benefits of which we'll list in the first section of this chapter.

In this chapter, we will cover the following topics:

  • Getting started with Ktor
  • Routing requests
  • Testing the service
  • Modularizing the application
  • Connecting to a database
  • Creating new entities
  • Making the test consistent
  • Fetching entities
  • Organizing routes in Ktor
  • Achieving concurrency in Ktor

By the end of this chapter, you'll have a microservice written in Kotlin that is well tested and can read data...

Technical requirements

This is what you'll need to get started:

  • JDK 11 or later
  • IntelliJ IDEA
  • Gradle 6.8 or later
  • PostgreSQL 14 or later

This chapter will assume that you have PostgreSQL already installed and that you have the basic knowledge for working with it. If you don't, please refer to the official documentation: https://www.postgresql.org/docs/14/tutorial-install.html.

You can find the source code for this chapter here: https://github.com/PacktPublishing/Kotlin-Design-Patterns-and-Best-Practices/tree/main/Chapter10.

Getting started with Ktor

You're probably tired of creating to-do or shopping lists.

So, instead, in this chapter, the microservice will be for a cat shelter. The microservice should be able to do the following:

  • Supply an endpoint we can ping to check whether the service is up and running
  • List the cats currently in the shelter
  • Provide us with a means to add new cats

The framework we'll be using for our microservice in this chapter is called Ktor. It's a concurrent framework that's developed and maintained by the creators of the Kotlin programming language.

Let's start by creating a new Kotlin Gradle project:

  1. From your IntelliJ IDEA, select File | New | Project and choose Kotlin from New Project and Gradle Kotlin as your Build System.
  2. Give your project a descriptive name – CatsHostel, in my case – and choose Project JDK (in this case, we are using JDK 15):

    Figure 10.1 – Selecting the Project JDK type...

Routing requests

Now, let's take a look at the routing block:

routing { 
    get("/") { 
        call.respondText("OK") 
    } 
}

This block describes all the URLs that will be handled by our server. In this case, we only handle the root URL. When that URL is requested, a text response, OK, will be returned to the user.

The following code returns a text response. Now, let's see how we can return a JSON response instead:

get("/status") {
    call.respond(mapOf("status" to "OK"))
}

Instead of using the respondText() method, we'll use respond(), which receives an object instead of a string. In our example, we're passing a map of strings to the respond() function. If we run this code, though, we'll get an exception.

This is because, by default, objects are not serialized into JSON. Multiple libraries...

Testing the service

To write our first test, let's create a new file called ServerTest.kt under the src/test/kotlin directory.

Now, let's add a new dependency:

dependencies {
    ...
    testImplementation("io.ktor:ktor-server-
        tests:$ktorVersion")
}

Next, let's add the following contents to our ServerTest.kt file:

internal class ServerTest {
    @Test
    fun testStatus() {
        withTestApplication {
            val response = handleRequest(HttpMethod.Get,                 "/status").response
            assertEquals(HttpStatusCode.OK,       ...

Modularizing the application

So far, our server has been started from the main() function. This was simple to set up, but this doesn't allow us to test our application.

In Ktor, the code is usually organized into modules. Let's rewrite our main function, as follows:

fun main() {
    embeddedServer(
        CIO,
        port = 8080,
        module = Application::mainModule
    ).start(wait = true)
}

Here, instead of providing the logic of our server within a block, we specified a module that will contain all the configurations for our server.

This module is defined as an extension function on the Application object:

fun Application.mainModule() {
    install(ContentNegotiation) {
        json()
    }
    ...

Connecting to a database

To store and retrieve cats, we'll need to connect to a database. We'll use PostgreSQL for that purpose, although using another SQL database won't be any different.

First, we'll need a new library to connect to the database. We'll use the Exposed library, which is also developed by JetBrains.

Let's add the following dependency to our build.gradle.kts file:

dependencies {
    implementation("org.jetbrains.exposed:exposed:0.17.14")
    implementation("org.postgresql:postgresql:42.2.24")
    ...
}

Once the libraries are in place, we need to connect to them. To do that, let's create a new file called DB.kt under /src/main/kotlin with the following contents:

object DB {
    private val host=System.getenv("DB_HOST")?:"localhost"
    private val port =     ...

Creating new entities

Our next task is adding the first cat to our virtual shelter.

Following the REST principles, it should be a POST request, where the body of the request may look something like this:

{"name": "Meatloaf", "age": 4}

We'll start by writing a new test:

@Test
fun `POST creates a new cat`() {
    ...
}

Backticks are a useful Kotlin feature that allows us to have spaces in the names of our functions. This helps us create descriptive test names.

Next, let's look at the body of our test:

withTestApplication(Application::mainModule) {
    val response = handleRequest(HttpMethod.Post, "/cats") {
        addHeader(
          HttpHeaders.ContentType,
          ContentType.Application.FormUrlEncoded.toString()
   ...

Making the tests consistent

Let's go back to our test and add the following piece of code:

@BeforeEach
fun setup() {
    DB.connect()
    transaction {
        SchemaUtils.drop(CatsTable)
    }
}

Here, we are using the @BeforeEach annotation on a function. As its name suggests, this code will run before each test. The function will establish a connection to the database and drop the table completely. Then, our application will recreate the table.

Now, our tests should pass consistently. In the next section, we'll learn how to fetch a cat from the database using the Exposed library.

Fetching entities

Following the REST practices, the URL for fetching all cats should be /cats, while for fetching a single cat, it should be /cats/123, where 123 is the ID of the cat we are trying to fetch.

Let's add two new routes for that:

get("/cats") {
    ...
}
get("/cats/{id}") {
    ...
}

The first route is very similar to the /status route we introduced earlier in this chapter. But the second round is slightly different: it uses a query parameter in the URL. You can recognize query parameters by the curly brackets around their name.

To read a query parameter, we can access the parameters map:

val id = requireNotNull(call.parameters["id"]).toInt()

If there is an ID on the URL, we need to try and fetch a cat from the database:

val cat = transaction {
    CatsTable.select {
        CatsTable.id.eq(id)
    ...

Organizing routes in Ktor

In this section, we'll see what the idiomatic approach in Ktor is for structuring multiple routes that belong to the same domain.

Our current routing block looks like this:

routing {
    get("/status") {
        ...
    }
    post("/cats") {
        ...    
    }
    get("/cats") {
        …
    }
    get("/cats/{id}") {
        ...    
    }
}

It would be good if we could extract all the routes that are related to cats into a separate file. Let's start by replacing all the cat routes with a function:

routing { 
    ...

Achieving concurrency in Ktor

Looking back at the code we've written in this chapter, you may be under the impression that the Ktor code is not concurrent at all. However, this couldn't be further from the truth.

All the Ktor functions we've used in this chapter are based on coroutines and the concept of suspending functions.

For every incoming request, Ktor will start a new coroutine that will handle it, thanks to the CIO server engine, which is based on coroutines at its core. Having a concurrency model that is performant but not obtrusive is a very important principle in Ktor.

In addition, the routing blocks we used to specify all our endpoints have access to CoroutineScope, meaning that we can invoke suspending functions within those blocks.

One of the examples for such a suspending function is call.respond(), which we were using throughout this chapter. Suspending functions provide our application with opportunities to context switch, and to execute...

Summary

In this chapter, we have built a well-tested service using Kotlin that uses the Ktor framework to store entities in the database. We've also discussed how the multiple design patterns that we encountered at the beginning of this book, such as Factory, Singleton, and Bridge, are used in the Ktor framework to provide a flexible structure for our code.

Now, you should be able to interact with the database using the Exposed framework. We've learned how we can declare, create, and drop tables, how to insert new entities, and how to fetch and delete them.

In the next chapter, we'll look at an alternative approach to developing web applications, but this time using a Reactive framework called Vert.x. This will allow us to compare the concurrent and Reactive approaches for developing web applications and discuss the tradeoffs of each of the approaches.

Questions

  1. How are the Ktor applications structured and what are their benefits?
  2. What are plugins in Ktor and what are they used for?
  3. What is the main problem that the Exposed library solves?
lock icon
The rest of the chapter is locked
You have been reading a chapter from
Kotlin Design Patterns and Best Practices - Second Edition
Published in: Jan 2022Publisher: PacktISBN-13: 9781801815727
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at €14.99/month. Cancel anytime

Author (1)

author image
Alexey Soshin

Alexey Soshin is a software architect with 15 years of experience in the industry. He started exploring Kotlin when Kotlin was still in beta, and since then has been a big enthusiast of the language. He's a conference speaker, published writer, and the author of a video course titled Pragmatic System Design.
Read more about Alexey Soshin