Jack Nicol


Kotlin Coroutines in Practice

Kotlin coroutines are well documented at the introductory level, but production usage surfaces patterns that tutorials tend to gloss over. Here are a few that have been useful.

Structured Concurrency Is Your Friend

The key mental model is that coroutines are scoped. If a parent scope is cancelled, all its children are cancelled too. This makes resource cleanup predictable.

coroutineScope {
    val result1 = async { fetchUserData(userId) }
    val result2 = async { fetchAccountBalance(userId) }
    process(result1.await(), result2.await())
}

If either fetch throws, the scope cancels the other — no dangling background work.

supervisorScope for Independent Tasks

When you want failures to be isolated rather than propagating to siblings, use supervisorScope:

supervisorScope {
    val jobs = userIds.map { id ->
        launch {
            runCatching { syncUser(id) }
                .onFailure { logger.error("Failed to sync user $id", it) }
        }
    }
}

One failing sync won’t kill the others.

Avoid GlobalScope

GlobalScope coroutines live as long as the application and aren’t tied to any lifecycle. In a service context, prefer injecting a CoroutineScope tied to the component’s lifecycle. Spring’s @Bean with a CoroutineScope backed by SupervisorJob works well.

@Bean
fun applicationScope(): CoroutineScope =
    CoroutineScope(SupervisorJob() + Dispatchers.Default)

Flow for Streaming Data

Flow is the coroutines equivalent of a reactive stream — cold, composable, and cancellable. It’s particularly useful for processing database result sets without loading everything into memory:

fun findActiveUsers(): Flow<User> = flow {
    database.streamActiveUsers().forEach { emit(it) }
}

Dispatcher Choice

  • Dispatchers.Default — CPU-bound work (computation, parsing)
  • Dispatchers.IO — blocking I/O (JDBC, file access)
  • Dispatchers.Unconfined — rarely needed outside testing

When in doubt, let your framework handle dispatch (Ktor and Spring both have coroutine-aware integrations).

Testing

Use runTest from kotlinx-coroutines-test for unit tests — it handles virtual time and makes testing delay-based logic straightforward without actually waiting.