skip to content

You are building a live-search feature. Compare collectLatest/flatMapLatest with conflate and explain how debounce fits in. When is collectLatest the wrong choice?

level: seniorimportance: should knowfreq 40%

answer

  1. flatMapLatest = cancel old request, run latest, no stale overwrite
  2. conflate = keep latest, never cancel running handler
  3. debounce = wait for typing pause, fewer queries
  4. Pipeline: debounce → distinctUntilChanged → flatMapLatest
  5. Wrong for must-complete side effects

basics

~20 s

For live search you want only the newest query to matter. collectLatest/flatMapLatest cancel the previous in-flight search when a new query arrives. conflate instead keeps the latest value but lets the current handler finish without cancelling. debounce waits for a pause in typing before emitting, cutting wasted requests.

solid answer

~50 s

In live search, each keystroke supersedes the prior query. flatMapLatest is the idiomatic operator: it cancels the previous inner request flow and starts a new one, so a slow earlier request is abandoned and won't overwrite fresher results — this also prevents out-of-order responses. collectLatest gives the same cancel-previous behavior at the terminal if you fire the request directly in the block. conflate is different: it never cancels the running handler; it just drops intermediate emissions while the collector is busy and delivers the most recent value when free — good when the work is cheap/non-cancellable but you can't keep up. debounce(timeout) suppresses bursts: it only emits a value after no new value for `timeout`, drastically reducing requests while typing. Typical pipeline: queries.debounce(300).distinctUntilChanged().flatMapLatest { search(it) }. collectLatest is wrong when the in-flight work has side effects that must complete (a payment, an analytics write) — cancelling mid-flight could leave partial state.

code

kotlin · 12 lines
kotlin
import kotlinx.coroutines.flow.*

fun searchResults(queries: Flow<String>, repo: SearchRepo): Flow<List<Hit>> =
    queries
        .debounce(300)
        .map { it.trim() }
        .filter { it.length >= 2 }
        .distinctUntilChanged()
        .flatMapLatest { q -> repo.search(q) } // cancels previous query

interface SearchRepo { fun search(q: String): Flow<List<Hit>> }
class Hit

go deeper

for a junior

Knows flatMapLatest/collectLatest cancel the previous search for live search.

for a middle

Builds the debounce → distinctUntilChanged → flatMapLatest pipeline and explains each stage.

for a senior

Contrasts conflate (no cancel) vs Latest (cancel), and articulates the out-of-order/stale-overwrite guarantee.

for a principal

Identifies when cancellation is unsafe (side effects, non-idempotent work) and designs an alternative (queue, collect, idempotency keys).

## The problem Live search emits a query per keystroke; only the **latest** query's results matter, and we want to minimize wasted network calls and avoid **out-of-order results** (a slow old request landing after a newer one). ## The three tools - **`collectLatest` / `flatMapLatest`** — *cancel the previous in-flight processing* when a newer value arrives. `flatMapLatest { q -> searchFlow(q) }` cancels the previous inner request flow's collection and starts the new query. This both saves the wasted request and guarantees the displayed result corresponds to the latest query. - **`conflate()`** — *no cancellation*. While the collector is busy, upstream values are buffered to capacity 1, keeping only the **most recent**; intermediate values are dropped. The current handler always finishes. Choose this when the per-value work is short and cannot/should-not be cancelled, but you still can't process every value. - **`debounce(timeoutMillis)`** — *time-based gating*. Emits a value only after `timeout` elapses with no newer value, collapsing a burst of keystrokes into one emission. It reduces how many queries even reach the search at all. ## How they compose ```kotlin val results: Flow<List<Hit>> = queryFlow .debounce(300) // wait for a typing pause .filter { it.length >= 2 } // skip trivial queries .distinctUntilChanged() // ignore no-op changes .flatMapLatest { q -> // cancel previous request on new query searchRepository.search(q) } ``` - `debounce` cuts the *number* of queries. - `distinctUntilChanged` removes redundant repeats. - `flatMapLatest` ensures only the **latest** query's request runs to completion. ## collectLatest vs flatMapLatest here Both cancel previous work. Use `flatMapLatest` when the search itself is a `Flow` (e.g. streaming or paged results) so you can chain operators on the result. Use `collectLatest` when you just fire a `suspend` call and update state in the terminal. ## When collectLatest is the wrong choice - **Side-effecting, must-complete work:** cancelling a half-done write, payment, file move, or non-idempotent mutation can leave partial state. Prefer `collect` (handle every value) or a queue. - **Every value matters** (audit logs, metrics): cancellation drops processing of superseded values — unacceptable. - **Non-cancellable work** (no suspension point): cancellation won't fire, so you gain nothing and may pile up. - For those, `conflate` (finish current, skip stale) or plain `collect` are safer. ## Out-of-order safety A subtle senior point: with plain `collect` + launching requests concurrently, a slow old request can resolve after a fast new one and overwrite the UI with stale data. `flatMapLatest`/`collectLatest` eliminate this by cancelling the old request entirely.

  • How does flatMapLatest prevent stale results overwriting fresh ones?
    It cancels the previous inner flow's collection when a new query arrives, so the old request's results never get emitted/applied.
  • Why pair debounce with distinctUntilChanged?
    debounce reduces burst frequency; distinctUntilChanged removes consecutive duplicate queries (e.g. text changed then reverted), avoiding redundant identical searches.
  • When would you pick conflate over collectLatest?
    When the per-value work is cheap and must finish (non-cancellable or side-effecting) but you still can't keep up — conflate keeps only the latest without cancelling the running handler.

debounce is waiting until someone stops talking before you reply; flatMapLatest is hanging up the old call the instant a new one rings.

saying these in an interview costs you the question

  • Using collectLatest for must-complete side-effecting work
  • Claiming conflate cancels the in-flight handler (it doesn't)
  • Thinking debounce and flatMapLatest are interchangeable
  • Ignoring the out-of-order/stale-result hazard that flatMapLatest solves
  • Forgetting distinctUntilChanged, causing duplicate searches

context