skip to content

Cold Semantics & Laziness

Cold and lazy means no work happens without a collector, each collection is an independent run, and emissions within one collector are sequential. Interviewers use this to check you would not expect two collectors to share a single upstream run.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What does it mean that a Kotlin Flow is "cold", and what does that imply about when its code runs?

level: juniorimportance: must knowfreq 80%

answer

  1. Nothing runs until collect
  2. flow {} = recipe, not execution
  3. Terminal operator triggers it
  4. Each collect re-runs from the top
  5. Cold vs hot (StateFlow/SharedFlow)

basics

~10 s

A cold Flow does nothing on its own. The code inside it runs only when you collect it. Until someone collects, no values are produced and no work happens.

solid answer

~40 s

A Flow is cold because the block passed to the flow {} builder is not executed when the Flow is created — it runs lazily, only when a terminal operator such as collect is called. Creating a Flow just builds a description (a recipe) of how to produce values; it is inert. Each call to collect starts the producer block from the beginning, so the flow is re-runnable. Contrast this with a hot source like a SharedFlow or StateFlow, which emits regardless of collectors. Cold flows are also sequential per collector: emit suspends until the collector finishes processing the previous value. This laziness means side effects (network calls, logging) inside flow {} are deferred until collection, which is the key mental model for reasoning about Flow.

code

kotlin · 7 lines
kotlin
fun numbers(): Flow<Int> = flow {
    println("producing")
    for (i in 1..3) { emit(i); delay(50) }
}

val f = numbers()      // prints nothing
f.collect { print(it) } // prints "producing" then 123

go deeper

for a junior

Knows nothing runs until collect and that flow {} is a deferred recipe.

for a middle

Distinguishes intermediate vs terminal operators and explains re-execution per collect.

for a senior

Contrasts cold vs hot, relates laziness to deferred side effects and resource management.

for a principal

Frames coldness as a design choice for composability and backpressure, and discusses where hot conversions belong in an architecture.

## What "cold" means In Kotlin coroutines, a `Flow<T>` is a **cold** asynchronous data stream. "Cold" means the **producer code does not run until the Flow is collected**. Creating a Flow with the `flow { ... }` builder, `flowOf(...)`, or `asFlow()` only constructs an object that *describes* how to produce values — it does not start producing them. - **Terminal operator** = an operator that actually triggers execution and consumes the stream, such as `collect`, `toList`, `first`, `single`, `reduce`, `fold`. It is `suspend` and must run inside a coroutine. - **Intermediate operator** = `map`, `filter`, `onEach`, `transform`, etc. These are also lazy: they just wrap the upstream Flow and return a new cold Flow. They run only when a terminal operator pulls values through. ## Why this matters ```kotlin fun fetchUsers(): Flow<User> = flow { println("start") // does NOT print here emit(api.getUser(1)) } val f = fetchUsers() // nothing prints, no network call f.collect { println(it) } // NOW "start" prints and the API is called ``` Because the block is deferred, **side effects are deferred too**. Logging, network requests, and database reads inside `flow {}` happen at collection time, not at construction time. ## Re-runnable on each collect Every call to `collect` runs the producer block **again, from the top**, independently. Two collectors get two separate executions: ```kotlin val ticks = flow { var i = 0 while (true) { emit(i++); delay(100) } } ticks.collect { ... } // its own counter starting at 0 ticks.collect { ... } // a fresh, independent counter starting at 0 ``` ## Sequential within one collector Inside a single collection, `emit` and the collector body run **sequentially**: `emit` suspends the producer until the collector has finished handling the current value, then resumes to produce the next. There is no concurrency between producer and consumer in a plain Flow. ## Cold vs hot - **Cold** (`flow {}`, `flowOf`, `asFlow`): per-collector execution, lazy, starts on collect. - **Hot** (`StateFlow`, `SharedFlow`, channels): exist and may emit independently of collectors; collectors share one running source. The practical takeaway: a Flow is a **lazy recipe**, not a running stream. Nothing executes until `collect`.

  • If you call map on a Flow but never collect it, does the map lambda run?
    No. map is an intermediate operator; it returns a new cold Flow. Its lambda only runs when a terminal operator collects the chain.
  • Name a terminal operator other than collect.
    toList, first, single, reduce, fold, or count — each starts the upstream and consumes values.

A cold Flow is like a recipe card: writing it down cooks nothing — only following it (collecting) produces the meal.

saying these in an interview costs you the question

  • Saying the flow {} block runs as soon as the Flow is created
  • Thinking map/filter execute immediately without a terminal operator
  • Confusing Flow with StateFlow (treating it as always-emitting)
  • Believing a Flow caches values from a previous collect

context

open as a page

Explain what happens when the same cold Flow is collected twice. Do the two collectors share emissions or state?

level: middleimportance: must knowfreq 65%

basics

~10 s

Each collect runs the Flow's code again from scratch, completely independently. The two collectors do not share values or any internal state — it is like running the recipe twice.

open as a page

Within a single collector, how do emit and the collector body interact? Are they concurrent or sequential?

level: middleimportance: should knowfreq 55%

basics

~10 s

They run one after another, not at the same time. emit suspends the producer until the collector finishes handling the current value, then the producer continues to the next emit.

open as a page

Compare cold Flow laziness with hot StateFlow/SharedFlow. When would you deliberately keep a flow cold, and when would you convert it to hot?

level: seniorimportance: should knowfreq 50%

basics

~20 s

Cold flows run per collector and only on collect, so they are great for on-demand, isolated work. Hot flows always exist and share one stream, good for shared state or events many parts observe at once.

open as a page

A teammate creates a Flow that logs and makes an HTTP call, but the logs never appear. The Flow is never collected. Explain why, and how coldness should shape where you put side effects.

level: seniorimportance: should knowfreq 45%

basics

~10 s

The Flow is cold, so its code only runs on collect. Since nobody collects it, the logging and HTTP call never execute. Side effects belong inside the flow and only fire when collected.

open as a page