Interview QuestionPractical QuestionFollow-up Questions

Channels and Deferred in Kotlin Coroutines

skydovesJaewoong Eum (skydoves)||9 min read

Channels and Deferred in Kotlin Coroutines

Kotlin Coroutines provide two distinct concurrency primitives for passing data between coroutines: Channel and Deferred. A Deferred<T> represents a single future result, while a Channel<T> represents a stream of values. Understanding how they differ and when to apply patterns like pipelines, fan-out, and fan-in is fundamental to building concurrent architectures. By the end of this lesson, you will be able to:

  • Explain the difference between Deferred and Channel in terms of cardinality and usage.
  • Build a pipeline using produce and channel chaining.
  • Describe the fan-out pattern for distributing work across multiple consumers.
  • Describe the fan-in pattern for aggregating results from multiple producers.
  • Choose between Deferred and Channel based on whether the use case involves a single result or a stream.

Deferred: A Single Future Result

Deferred<T> is produced by async and represents exactly one value that will be available in the future. Calling await() suspends the caller until the result is ready. Once the value is produced, any subsequent call to await() returns the same result immediately.

val deferred: Deferred<String> = async {
    fetchUserName(userId)
}
val name: String = deferred.await()

Deferred fits situations where a coroutine performs a computation or network call and returns one result. It is analogous to a Future or Promise in other languages. Unlike a callback based approach, await() integrates with structured concurrency. If the parent scope is cancelled, the Deferred is cancelled too.

Channel: A Stream of Values

A Channel<T> allows coroutines to send and receive a stream of values. It behaves like a suspending queue: send() suspends when the channel is full, and receive() suspends when the channel is empty. Channels support multiple capacity strategies:

  • Rendezvous (capacity 0): The sender suspends until a receiver is ready, and vice versa. This enforces strict handoff between producer and consumer.
  • Buffered: A fixed size buffer allows the sender to proceed without waiting as long as the buffer is not full.
  • Conflated: Only the most recent value is kept. If the consumer is slow, intermediate values are dropped.
  • Unlimited: An unbounded buffer that never suspends the sender, but can consume unbounded memory if the consumer falls behind.

Channels are useful when data flows continuously between coroutines, such as in producer-consumer or pipeline architectures. The choice of capacity strategy affects backpressure behavior and memory usage.

Building a Pipeline

Pipelines chain coroutines that communicate through channels. Each stage consumes from one channel, transforms the data, and optionally sends it to another channel. The produce builder creates a coroutine that sends values into a ReceiveChannel.

fun CoroutineScope.produceSquares():
  ReceiveChannel<Int> = produce {
    for (x in 1..5) send(x * x)
}

This interview continues for subscribers

Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.

Become a Sponsor