skip to content

You expose a producer's output as receiveAsFlow() and two coroutines collect it concurrently. Each collector expects to see every element. Why is this broken, and how do you fix it?

level: middleimportance: should knowfreq 45%

answer

  1. Channel fan-out, not broadcast
  2. Collectors compete -> split elements
  3. Broadcast = SharedFlow / shareIn
  4. Latest-value broadcast = StateFlow
  5. shareIn(scope, SharingStarted, replay)

basics

~20 s

A Channel gives each item to only one receiver, so the two collectors split the elements instead of both seeing them all. To give every collector the full stream, use a SharedFlow (or shareIn) instead of a raw channel.

solid answer

~40 s

`receiveAsFlow()` preserves the Channel's single-consumption semantics: each emitted element is received by exactly one collector, so two concurrent collectors **compete** and split the stream (fan-out) rather than each getting a full copy. That's correct behavior for a channel but wrong for the 'both see everything' expectation. The fix is a **hot broadcast** abstraction: convert to a `SharedFlow` via `shareIn(scope, started, replay)`, or model the source as a `MutableSharedFlow` and `emit` to it. Each `SharedFlow` collector gets its own copy of every element. If you need the latest value plus updates, use `StateFlow`. Do not try to fix it by adding more channels manually unless you intentionally want fan-out.

code

kotlin · 10 lines
kotlin
// BROKEN expectation: both collectors want all values, but they compete
val flow = channel.receiveAsFlow()
launch { flow.collect { println("A:$it") } }
launch { flow.collect { println("B:$it") } } // A and B SPLIT elements

// FIX: multicast with shareIn
val shared = channel.receiveAsFlow()
    .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 0)
launch { shared.collect { println("A:$it") } }
launch { shared.collect { println("B:$it") } } // both see every element

go deeper

for a junior

Recognizes the two collectors don't both get everything and that some broadcast mechanism is needed.

for a middle

Names SharedFlow/shareIn as the fix and explains fan-out vs broadcast clearly.

for a senior

Discusses SharingStarted policies, replay/buffer sizing, and when fan-out is the intended design instead of a bug.

for a principal

Weighs StateFlow vs SharedFlow vs channel for the actual product requirement, backpressure and lifecycle (scope) implications of shareIn.

## Why it's broken A plain `Channel<T>` is **single-consumption**: an element sent with `send()` is delivered to **exactly one** `receive()`. `receiveAsFlow()` faithfully exposes that. So when two coroutines collect the same `receiveAsFlow()` Flow at once, they **compete** for elements — element 1 might go to collector A, element 2 to collector B, etc. This is **fan-out**, useful for distributing work, but it is **not** what 'every collector sees every element' requires. ## What you actually want 'Every collector sees every element' is **broadcast / multicast** semantics. Kotlin's hot multicast primitive is `SharedFlow` (and its stateful variant `StateFlow`). Each collector of a `SharedFlow` receives its own copy of every emission. ## Fix 1 — shareIn ```kotlin val upstream: Flow<Int> = channel.receiveAsFlow() val shared: SharedFlow<Int> = upstream.shareIn( scope = scope, started = SharingStarted.WhileSubscribed(), replay = 0, ) // every collector of `shared` now gets every element ``` `shareIn` launches one collection of the upstream in `scope` and multicasts it. `SharingStarted` controls when the upstream runs (`Eagerly`, `Lazily`, `WhileSubscribed`). `replay` controls how many recent values late subscribers get. ## Fix 2 — MutableSharedFlow directly ```kotlin val events = MutableSharedFlow<Int>(replay = 0, extraBufferCapacity = 64) // producer: events.emit(value) // every collector of events.asSharedFlow() sees every value ``` ## Fix 3 — StateFlow for current-value semantics If collectors need the *latest* value immediately plus subsequent updates, use `MutableStateFlow`/`StateFlow`; it conflates and always has a current value. ## Summary - Channel / `receiveAsFlow()` -> **fan-out** (one element, one collector). - `SharedFlow` / `shareIn` -> **broadcast** (one element, all collectors). - `StateFlow` -> broadcast of the latest value with conflation. Pick by whether you want to **split** work or **duplicate** the stream.

  • When is the competing fan-out behavior actually the right design?
    When distributing work across multiple consumers — e.g., a job queue where each task should be handled by exactly one worker.
  • What does SharingStarted.WhileSubscribed() change versus Eagerly?
    WhileSubscribed only runs the upstream while there is at least one collector and can stop it when none remain; Eagerly starts immediately and keeps it running.

A channel is a single ticket queue: each customer is served once. SharedFlow is a radio broadcast: every listener hears the same thing.

saying these in an interview costs you the question

  • Claims receiveAsFlow already broadcasts and the code is fine
  • Tries to 'fix' it by collecting twice expecting duplication
  • Confuses fan-out with broadcast
  • Suggests StateFlow when full element history per collector is needed but never mentions replay/conflation
  • Doesn't know SharedFlow/shareIn exist

context