Spring boot supports a non-blocking programming model with the spring-webflux module. Webflux supports a Reactive API using the Reactor library Flux and Mono API types. This model forces you to write your code in a different style than most people are used to. It generally is much harder to follow and debug.

This post shows you a short example of using Kotlin’s Coroutines instead of Reactor to write your code in a more imperative and easier to follow style. It includes examples of using Retrofit as HTTP client to access external API’s concurrently in a non-blocking way.

Retrofit is the de facto standard HTTP client for Android and widely used for Java and Kotlin microservices as well. It has built-in support for coroutines and adapters for most popular api types!

Short intro to coroutines

In short, coroutines are lightweight threads. You can launch coroutines using the launch and async coroutine builders. By marking your functions as suspend functions which allows the library to 'suspend' the execution of a coroutine to continue at a later time. Suspend functions can start in thread 1, then suspend, and later continue in thread 2. This all happens automatically, allowing you to write code in an imperative way, but executing it in an asynchronous way. This makes concurrent code easier to read, write and maintain.

For more information, please read the official guide https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html

Spring Boot configuration

When starting a new Kotlin project using Spring Boot, make sure to include spring-webflux and include coroutines support. The code below contains an example build.gradle in the Kotlin DSL instead of Groovy (the kotlin DSL is not a requirement).

build.gradle.kts

plugins {
	id("org.springframework.boot") version "2.3.5.RELEASE"
	id("io.spring.dependency-management") version "1.0.10.RELEASE"
	kotlin("jvm") version "1.4.10"
	kotlin("plugin.spring") version "1.4.10"
}

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-webflux")
    // The following two dependencies are required to enable coroutine support
	implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
	implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")

	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

	implementation("com.squareup.retrofit2:retrofit:2.9.0")
	implementation("com.squareup.retrofit2:converter-jackson:2.9.0")

	testImplementation("org.springframework.boot:spring-boot-starter-test") {
		exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
	}
	testImplementation("io.projectreactor:reactor-test")
}

tasks.withType<Test> {
	useJUnitPlatform()
}

/**
 * The code below instructs kotlin to target verion 11 of the jvm bytecode instead of the default 1.6.
 *
 * Without this code, you can expect the error:
 *
 * Cannot inline bytecode built with JVM target 1.8 into bytecode that is being built with JVM target 1.6.
 * Please specify proper '-jvm-target' option
 */
tasks.withType<KotlinCompile> {
	kotlinOptions {
		freeCompilerArgs = listOf("-Xjsr305=strict")
		jvmTarget = "11"
	}
}

Defining a HTTP client

For the sake of this blog post, we will use a JSON dummy REST service publicly available at https://jsonplaceholder.typicode.com/ . We will use the post and todo endpoints!

Let’s define the response types of these API’s:

data class PostDto(
    val userId: Long,
    val id: Long,
    val title: String,
    val body: String,
)

data class TodoDto(
    val userId: Long,
    val id: Long,
    val title: String,
    val completed: Boolean,
)

Let’s now start with creating a Retrofit client that exposes two suspend functions that can be called from within a coroutine context in a non-blocking fashion:

interface JsonPlaceHolderSuspendClient {
    @GET("/posts/{id}")
    suspend fun post(@Path("id") id: Int): PostDto

    @GET("/todos/{id}")
    suspend fun todo(@Path("id") id: Int): TodoDto
}

Of course, many projects already have a REST client in use and it’s not possible to change the function signature to a suspend function. Or you also use the client in a Java environment which does not support suspend!

To show you how to integrate in such environments. Let’s also define a client using the default Retrofit return type Call. A call can be executing in a blocking style by calling .execute() on it or asynchronously via .enqueue().

interface JsonPlaceHolderRegularClient {
    @GET("/posts/{id}")
    fun post(@Path("id") id: Int): Call<PostDto>

    @GET("/todos/{id}")
    fun todo(@Path("id") id: Int): Call<TodoDto>
}

Now let’s create the actual clients and expose them as beans in the configuration.

@SpringBootApplication
class SpringBootCoroutinesApplication {

	@Bean
	fun jsonPlaceHolderRegularClient(): JsonPlaceHolderRegularClient {
		return Retrofit.Builder()
			.baseUrl("https://jsonplaceholder.typicode.com")
			.addConverterFactory(JacksonConverterFactory.create(ObjectMapper().findAndRegisterModules()))
			.build()
			.create(JsonPlaceHolderRegularClient::class.java)
	}

	@Bean
	fun jsonPlaceHolderSuspendClient(): JsonPlaceHolderSuspendClient {
		return Retrofit.Builder()
			.baseUrl("https://jsonplaceholder.typicode.com")
			.addConverterFactory(JacksonConverterFactory.create(ObjectMapper().findAndRegisterModules()))
			.build()
			.create(JsonPlaceHolderSuspendClient::class.java)
	}
}

Create a simple REST controller that fetches 5 posts and 5 todos

Now let’s create a controller endpoint that fetches 5 posts and 5 todos asynchronously without blocking the request thread. The log statements will prove that the controller executes the requests on a different threads and that the request thread itself is not blocked.

also print the coroutine name by providing the VM argument -Dkotlinx.coroutines.debug="on".
data class OverviewDto(
    val posts: List<PostDto>,
    val todos: List<TodoDto>,
)

@RestController
class Controller (val jsonPlaceHolderSuspendClient: JsonPlaceHolderSuspendClient){

    @GetMapping("/overview-suspend")
    suspend fun getOverviewSuspend() :OverviewDto {
        log.info("fetching overview")
        // switch to the IO context for asynchronous IO related work!
        return withContext(Dispatchers.IO) {
            val postRequests: List<Deferred<PostDto>> = (1..5).map { id ->
                async {
                    log.info("fetching post $id")
                    jsonPlaceHolderSuspendClient.post(id).also {
                        log.info("done fetching post $id")
                    }
                }
            }
            val fiveTodosRequests: List<Deferred<TodoDto>> = (1..5).map { id ->
                async {
                    log.info("fetching todo $id")
                    jsonPlaceHolderSuspendClient.todo(id).also {
                        log.info("done fetching todo $id")
                    }
                }
            }

            // .awaitAll() maps over the list and calls await() on each Deferred object. It also cancelles
            // all the other tasks that are still rune if one of them fails!
            val postResponses = postRequests.awaitAll()
            val todoResponses = todoRequests.awaitAll()

            OverviewDto(
                posts = postResponses,
                todos = todoResponses,
            )
        }.also {
            log.info("done!")
        }
    }
}

If we start the application and call http://localhost:8080/overview-suspend. We get five posts and five todos back in the response. The logging shows us:

  1. that we start in the reactor thread: reactor-http-epoll-2 @coroutine#1.

  2. the requests are spawned as different coroutines on a pool of worker threads in a non-blocking fashion.

  3. the request is completed by thread DefaultDispatcher-worker-2 @coroutine#1

21:22:44.377 INFO  [reactor-http-epoll-2 @coroutine#1] n.t.s.Controller - fetching overview suspend
21:22:44.385 INFO  [DefaultDispatcher-worker-3 @coroutine#2] n.t.s.Controller - fetching post 1
21:22:44.387 INFO  [DefaultDispatcher-worker-5 @coroutine#5] n.t.s.Controller - fetching post 4
21:22:44.387 INFO  [DefaultDispatcher-worker-2 @coroutine#3] n.t.s.Controller - fetching post 2
21:22:44.391 INFO  [DefaultDispatcher-worker-7 @coroutine#6] n.t.s.Controller - fetching post 5
21:22:44.391 INFO  [DefaultDispatcher-worker-4 @coroutine#4] n.t.s.Controller - fetching post 3
21:22:44.399 INFO  [DefaultDispatcher-worker-11 @coroutine#9] n.t.s.Controller - fetching todo 3
21:22:44.399 INFO  [DefaultDispatcher-worker-9 @coroutine#11] n.t.s.Controller - fetching todo 5
21:22:44.399 INFO  [DefaultDispatcher-worker-12 @coroutine#10] n.t.s.Controller - fetching todo 4
21:22:44.398 INFO  [DefaultDispatcher-worker-8 @coroutine#8] n.t.s.Controller - fetching todo 2
21:22:44.399 INFO  [DefaultDispatcher-worker-6 @coroutine#7] n.t.s.Controller - fetching todo 1
21:22:44.772 INFO  [DefaultDispatcher-worker-11 @coroutine#10] n.t.s.Controller - done fetching todo 4
21:22:44.772 INFO  [DefaultDispatcher-worker-5 @coroutine#9] n.t.s.Controller - done fetching todo 3
21:22:44.772 INFO  [DefaultDispatcher-worker-12 @coroutine#11] n.t.s.Controller - done fetching todo 5
21:22:44.772 INFO  [DefaultDispatcher-worker-2 @coroutine#2] n.t.s.Controller - done fetching post 1
21:22:44.772 INFO  [DefaultDispatcher-worker-8 @coroutine#7] n.t.s.Controller - done fetching todo 1
21:22:44.854 INFO  [DefaultDispatcher-worker-2 @coroutine#6] n.t.s.Controller - done fetching post 5
21:22:44.855 INFO  [DefaultDispatcher-worker-2 @coroutine#4] n.t.s.Controller - done fetching post 3
21:22:44.855 INFO  [DefaultDispatcher-worker-2 @coroutine#8] n.t.s.Controller - done fetching todo 2
21:22:44.856 INFO  [DefaultDispatcher-worker-2 @coroutine#3] n.t.s.Controller - done fetching post 2
21:22:44.859 INFO  [DefaultDispatcher-worker-2 @coroutine#5] n.t.s.Controller - done fetching post 4
21:22:44.860 INFO  [DefaultDispatcher-worker-2 @coroutine#1] n.t.s.Controller - done!

We can check that it is indeed non-blocking by inspecting the logs. Take post 1 for example:

21:22:44.385 INFO [DefaultDispatcher-worker-3 @coroutine#2] n.t.s.Controller - fetching post 1 21:22:44.772 INFO [DefaultDispatcher-worker-2 @coroutine#2] n.t.s.Controller - done fetching post 1

The request starts in thread DefaultDispatcher-worker-3 and comes back in thread DefaultDispatcher-worker-2.

Using the regular client without suspend functions

Does this still work if our client is using Retrofits standard Call API?

Yes! Retrofit provides an extension method on Call called .awaitResponse(). It will non-blockingly wait for the response. Super easy! Let’s test this by implementing another controller method that uses that API:

@RestController
class Controller (val jsonPlaceHolderSuspendClient: JsonPlaceHolderSuspendClient, val jsonPlaceHolderSuspendClient: JsonPlaceHolderSuspendClient){

...

    @GetMapping("/overview-regular")
    suspend fun getOverviewRegular() :OverviewDto {
        log.info("fetching overview regular")
        return withContext(Dispatchers.IO) {
            val fivePosts = (1..5).map { id ->
                async {
                    log.info("fetching post $id")
                    jsonPlaceHolderRegularClient.post(id).awaitResponse().body()!!.also {
                        log.info("done fetching post $id")
                    }
                }
            }
            val fiveTodos = (1..5).map { id ->
                async {
                    log.info("fetching todo $id")
                    jsonPlaceHolderRegularClient.todo(id).awaitResponse().body()!!.also {
                        log.info("done fetching todo $id")
                    }
                }
            }

            OverviewDto(
                posts = fivePosts.awaitAll(),
                todos = fiveTodos.awaitAll(),
            )
        }
    }
}

The logs show the exact same result:

21:46:55.919 INFO  [reactor-http-epoll-2 @coroutine#1] n.t.s.Controller - fetching overview regular
21:46:55.927 INFO  [DefaultDispatcher-worker-3 @coroutine#2] n.t.s.Controller - fetching post 1
21:46:55.929 INFO  [DefaultDispatcher-worker-4 @coroutine#4] n.t.s.Controller - fetching post 3
21:46:55.930 INFO  [DefaultDispatcher-worker-6 @coroutine#6] n.t.s.Controller - fetching post 5
21:46:55.930 INFO  [DefaultDispatcher-worker-2 @coroutine#3] n.t.s.Controller - fetching post 2
21:46:55.931 INFO  [DefaultDispatcher-worker-5 @coroutine#5] n.t.s.Controller - fetching post 4
21:46:55.933 INFO  [DefaultDispatcher-worker-10 @coroutine#7] n.t.s.Controller - fetching todo 1
21:46:55.933 INFO  [DefaultDispatcher-worker-9 @coroutine#8] n.t.s.Controller - fetching todo 2
21:46:55.939 INFO  [DefaultDispatcher-worker-13 @coroutine#11] n.t.s.Controller - fetching todo 5
21:46:55.939 INFO  [DefaultDispatcher-worker-7 @coroutine#10] n.t.s.Controller - fetching todo 4
21:46:55.940 INFO  [DefaultDispatcher-worker-11 @coroutine#9] n.t.s.Controller - fetching todo 3
21:46:56.305 INFO  [DefaultDispatcher-worker-9 @coroutine#3] n.t.s.Controller - done fetching post 2
21:46:56.305 INFO  [DefaultDispatcher-worker-7 @coroutine#6] n.t.s.Controller - done fetching post 5
21:46:56.305 INFO  [DefaultDispatcher-worker-4 @coroutine#11] n.t.s.Controller - done fetching todo 5
21:46:56.305 INFO  [DefaultDispatcher-worker-6 @coroutine#9] n.t.s.Controller - done fetching todo 3
21:46:56.305 INFO  [DefaultDispatcher-worker-13 @coroutine#10] n.t.s.Controller - done fetching todo 4
21:46:56.391 INFO  [DefaultDispatcher-worker-7 @coroutine#4] n.t.s.Controller - done fetching post 3
21:46:56.393 INFO  [DefaultDispatcher-worker-7 @coroutine#2] n.t.s.Controller - done fetching post 1
21:46:56.396 INFO  [DefaultDispatcher-worker-7 @coroutine#5] n.t.s.Controller - done fetching post 4
21:46:56.396 INFO  [DefaultDispatcher-worker-4 @coroutine#7] n.t.s.Controller - done fetching todo 1
21:46:56.397 INFO  [DefaultDispatcher-worker-7 @coroutine#8] n.t.s.Controller - done fetching todo 2
21:46:56.397 INFO  [DefaultDispatcher-worker-7 @coroutine#1] n.t.s.Controller - done!

Conclusion

Writing non-blocking concurrent code is hard but uses fewer resources and scales better. The reactive APIs force developers to write code in a way that does not feel natural at first. Kotlin’s coroutines allow you to structure your concurrency in more imperative/natural way.

The support for coroutines by Spring webflux and clients such as Retrofit make it easy to make your code non-blocking without paying much in readability.

shadow-left