skip to content

flatMapConcat / flatMapMerge / flatMapLatest

When each element produces its own flow, flatMapConcat runs them in order, flatMapMerge runs them concurrently, and flatMapLatest cancels the previous inner flow on each new value. flatMapLatest is the search-as-you-type answer interviewers are usually fishing for.

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

questions

5

What problem do flatMapConcat, flatMapMerge, and flatMapLatest solve in Kotlin Flow, and how do they differ at a high level?

level: juniorimportance: must knowfreq 70%

answer

  1. Flow<Flow<T>> -> flatten to Flow<T>
  2. Concat = sequential, ordered
  3. Merge = concurrent, interleaved
  4. Latest = cancel previous inner flow
  5. Merge default concurrency 16

basics

~20 s

Each emitted value can itself produce a flow. These operators flatten those inner flows into one stream. Concat runs them one after another, Merge runs them at the same time, Latest cancels the old inner flow when a new value comes.

solid answer

~40 s

When a transform on a Flow returns another Flow, you get a Flow<Flow<T>>. The flatMap* operators flatten that nested structure into a single Flow<T>. flatMapConcat collects each inner flow to completion before starting the next, preserving order (sequential). flatMapMerge collects multiple inner flows concurrently up to a concurrency limit (default DEFAULT_CONCURRENCY = 16), interleaving their emissions, so order is not guaranteed. flatMapLatest cancels the currently running inner flow as soon as the upstream emits a new value and starts a fresh inner flow for it — ideal for 'use only the latest input' cases like search-as-you-type. All three are intermediate operators, are cold, and run inside a suspend context, so no manual threading is needed.

code

kotlin · 10 lines
kotlin
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.delay

fun query(n: Int) = flow { delay(50L); emit(n * 10) }

suspend fun demo() {
    flowOf(1, 2, 3).flatMapConcat { query(it) }.collect(::println)  // 10,20,30 in order
    flowOf(1, 2, 3).flatMapMerge { query(it) }.collect(::println)   // 10,20,30 but order not guaranteed
    flowOf(1, 2, 3).flatMapLatest { query(it) }.collect(::println)  // likely just 30
}

go deeper

for a junior

Can state that the three flatten flows-of-flows and give the one-line difference (sequential / concurrent / cancel-previous).

for a middle

Knows map produces a nested Flow, names flattenConcat/flattenMerge, and picks the right operator per scenario.

for a senior

Discusses ordering guarantees, the concurrency parameter and its default, and cancellation/structured-concurrency implications.

for a principal

Reasons about backpressure, resource exhaustion from unbounded concurrency, and when a custom transform/channelFlow beats a stock operator.

## The problem: flows of flows A `Flow<T>` is a cold asynchronous stream. Sometimes a transformation of each element itself produces a `Flow`. For example, for each user id you call `fetchProfile(id): Flow<Profile>`. If you use `map`, you get a `Flow<Flow<Profile>>` — a nested flow. **Flattening** operators turn `Flow<Flow<T>>` into a single `Flow<T>`. Kotlin offers three flattening strategies: ## flatMapConcat — sequential Collects each inner flow **to completion** before subscribing to the next one. Emissions stay in upstream order. Equivalent to `map { ... }.flattenConcat()`. ## flatMapMerge — concurrent Subscribes to several inner flows **at once** and merges their emissions as they arrive, so order is interleaved/non-deterministic. It takes a `concurrency` parameter limiting how many inner flows run simultaneously (default `DEFAULT_CONCURRENCY` = 16, overridable via the `kotlinx.coroutines.flow.defaultConcurrency` system property). ## flatMapLatest — keep only the newest When upstream emits a new value, the **currently running inner flow is cancelled** and a new one starts. Only the latest input's inner flow survives. Built on `transformLatest`. ```kotlin import kotlinx.coroutines.flow.* import kotlinx.coroutines.delay val source = flowOf(1, 2, 3) fun inner(n: Int) = flow { delay(100L); emit("$n-a"); delay(100L); emit("$n-b") } source.flatMapConcat { inner(it) } // 1-a,1-b,2-a,2-b,3-a,3-b (ordered) source.flatMapMerge { inner(it) } // interleaved: 1-a,2-a,3-a,1-b,... source.flatMapLatest { inner(it) } // only 3-a,3-b (1 and 2 cancelled) ``` ## Key terms - **Cold**: nothing runs until a terminal operator (e.g. `collect`) subscribes. - **Intermediate operator**: returns a new Flow, runs lazily, does not start collection. - **Inner flow**: the flow returned by your lambda for each upstream value. - **Concurrency**: number of inner flows collected at the same time. All three are `suspend`-friendly and structured-concurrency aware: cancelling the collector cancels the inner flows.

  • If your lambda returns a Flow and you use map instead of flatMapConcat, what type do you get?
    Flow<Flow<T>> — a nested flow you'd then have to flatten yourself with flattenConcat/flattenMerge.
  • Which one would you pick for search-as-you-type?
    flatMapLatest, so each new keystroke cancels the in-flight request for the previous query.

Concat = one checkout lane served fully before the next; Merge = several lanes open at once; Latest = abandon your half-rung order the moment a new one arrives.

saying these in an interview costs you the question

  • Thinking flatMapMerge preserves emission order
  • Confusing flatMap* with map (not realizing map gives nested flows)
  • Believing flatMapConcat runs inner flows in parallel
  • Saying flatMapLatest waits for the old inner flow to finish before cancelling
  • Claiming these are terminal operators that start collection

context

open as a page

Explain the ordering and concurrency guarantees of flatMapConcat versus flatMapMerge, including the concurrency argument and its default.

level: middleimportance: must knowfreq 60%

basics

~20 s

Concat processes inner flows one at a time, so output keeps the input order. Merge processes several at once, so faster inner flows can finish first and outputs interleave. Merge lets you cap how many run together; the default is 16.

open as a page

Given a few real scenarios (parallel HTTP fan-out, ordered pagination, autocomplete), which flattening operator fits each and why?

level: middleimportance: should knowfreq 55%

basics

~10 s

Autocomplete uses flatMapLatest (drop stale searches). Ordered pagination uses flatMapConcat (keep order). Parallel HTTP fan-out uses flatMapMerge with a concurrency cap (run several at once but bounded).

open as a page

How does flatMapLatest implement cancellation, and what must your inner flow do to honour it correctly?

level: seniorimportance: should knowfreq 50%

basics

~20 s

When a new value arrives, flatMapLatest cancels the coroutine running the previous inner flow and starts a new one. Your inner flow must be cancellation-cooperative — use suspending calls or check for cancellation — so it actually stops.

open as a page

What are flattenConcat/flattenMerge, how do they relate to the flatMap* operators, and what should you know about the experimental/stability status of these operators?

level: seniorimportance: nice to knowfreq 30%

basics

~10 s

flattenConcat and flattenMerge flatten an existing Flow<Flow<T>>. flatMapConcat/Merge are just map-then-flatten shortcuts. Some of these operators were experimental and need an opt-in annotation; check your kotlinx-coroutines version.

open as a page