Are Coroutines a Superior Option in Kotlin Compared to Reactive Streams?

As asynchronous programming continues to shape modern software development, the choice of tools becomes crucial. Reactive programming offers impressive performance and efficiency, but it comes with added complexity that can obscure your code's readability. In this article, we will delve into how Kotlin Coroutines offer a simpler, more intuitive approach for asynchronous and non-blocking programming compared to Reactive Streams.


Understanding the Temptation and Complexity of Reactive Programming

Reactive programming is undeniably tempting for developers striving to handle higher traffic with the same hardware resources. However, this paradigm's increased code complexity and potential for hard-to-detect bugs can outweigh its benefits. Let's illustrate this with a sample scenario.


Sample Scenario

Imagine you are developing a website akin to Wikipedia, which caters to both logged-in and anonymous users, while implementing mechanisms to block misbehaving users. Our focus will be the backend code responsible for returning a welcome message displayed on the main page:


class WelcomeService {
fun welcome(usernameOrIp: String): WelcomeMessage {
val userProfile: UserProfile? = findUserProfile(usernameOrIp)
val block: Block? = findBlock(usernameOrIp)
return generateWelcome(usernameOrIp, userProfile, block)
}
}

The above code is straightforward, representing the business logic efficiently. It allows users to maintain their personal information while enabling administrators to block users if necessary. The generateWelcome function utilizes Kotlin's nullability features to handle scenarios where certain user information might be absent.


Examining the Welcome Message Logic

fun generateWelcome(
usernameOrIp: String,
userProfile: UserProfile?,
block: Block?
): WelcomeMessage =
when {
block != null -> WelcomeMessage(
"You are blocked. Reason: ${block.reason}",
WARNING
)
else -> WelcomeMessage(
"Hello ${userProfile?.fullName ?: usernameOrIp}",
INFO
)
}

This function effectively manages null scenarios, demonstrating Kotlin's capability to express the intent with minimal ceremony. But let's see how the same would be handled in a reactive style.


Reactive Code: A Look at Increased Complexity

fun welcome(usernameOrIp: String): Mono {
return userProfileRepository.findById(usernameOrIp)
.zipWith(blockRepository.findById(usernameOrIp))
.map { tuple ->
generateWelcome(usernameOrIp, tuple.t1, tuple.t2)
}
}

This snippet showcases the transition to a reactive style using the Reactor library. It combines two reactive streams with a zip operator to produce a tuple. However, as straightforward as it looks, it introduces a problem when any input stream is empty, leading to a silent failure with no exceptions thrown.


Addressing the Silent Failure in Reactive Code

fun welcome(usernameOrIp: String): Mono {
return userProfileRepository.findById(usernameOrIp)
.map { Optional.of(it) }
.defaultIfEmpty(Optional.empty())
.zipWith(blockRepository.findById(usernameOrIp)
.map { Optional.of(it) }
.defaultIfEmpty(Optional.empty()))
.map { tuple ->
generateWelcome(
usernameOrIp, tuple.t1.orElse(null), tuple.t2.orElse(null)
)
}
}

Although this version rectifies the problem, it introduces extra verbosity and loses the original simplicity and readability. The domain-specific terms are overshadowed by reactive jargon like 'Optional' and 't1'. Such complexity can hinder code maintainability and understanding for those unfamiliar with the system.


Enhancing Readability with Kotlin Coroutines

Kotlin Coroutines offer a more elegant solution to asynchronous programming. Coroutines allow writing concurrent Kotlin code similar to traditional imperative, sequential code through the use of suspended functions.


Using Coroutines for Asynchronous Operations

Let's transform the reactive code into a coroutine-based approach:


suspend fun welcome(usernameOrIp: String): WelcomeMessage {
val userProfile = userProfileRepository.findById(usernameOrIp).awaitFirstOrNull()
val block = blockRepository.findById(usernameOrIp).awaitFirstOrNull()
return generateWelcome(usernameOrIp, userProfile, block)
}

This method closely mirrors the original blocking version but leverages non-blocking execution. The awaitFirstOrNull Kotlin extension allows smooth transformation of Mono instances to nullable types.


Integrating Coroutines with Spring WebFlux

Spring WebFlux supports coroutines out-of-the-box, allowing you to define asynchronous endpoints effortlessly:


@RestController
class WelcomeController(
private val welcomeService: WelcomeService
) {
@GetMapping("/welcome")
suspend fun welcome(@RequestParam ip: String) =
welcomeService.welcome(ip)
}

For more details on the integration, refer to the Kotlin support section in Spring Framework documentation.


Maintaining Flexibility: Mixed Approaches

Even if you prefer to work with reactive streams, you can leverage suspending functions internally to handle nullability more intuitively. Using the mono function from the kotlinx-coroutines-reactor library enables you to integrate coroutine-based logic in reactive environments:


fun welcome(usernameOrIp: String): Mono {
return mono {
val userProfile = userProfileRepository.findById(usernameOrIp).awaitFirstOrNull()
val block = blockRepository.findById(usernameOrIp).awaitFirstOrNull()
generateWelcome(usernameOrIp, userProfile, block)
}
}

The kotlinx-coroutines-rx2 library provides similar functionality for RxJava 2 types, maintaining flexibility in your project choice.


Conclusion

Kotlin Coroutines provide a more straightforward and readable approach to asynchronous programming compared to Reactive Streams. They preserve Kotlin's nullability advantages, simplify error handling, and allow you to write codes in a familiar, imperative style. A hybrid approach is also feasible, marrying the benefits of coroutines with reactive programming frameworks.


By leveraging coroutines, you can enhance code maintainability, reduce the cognitive load of understanding asynchronous operations, and maintain the integrity of Kotlin’s expressive power.