skip to content

Both `produce` and `actor` are flagged experimental/obsolete. As a principal engineer, when would you still use them, and what modern alternatives would you reach for instead?

level: principalimportance: nice to knowfreq 18%

answer

  1. produce = ExperimentalCoroutinesApi, actor = ObsoleteCoroutinesApi
  2. values -> Flow (cold); hot/pipeline -> produce
  3. state mutex -> Mutex.withLock
  4. observable state -> StateFlow.update (CAS)
  5. command queue + replies -> keep actor pattern

basics

~20 s

produce is good for hot, channel-based streaming pipelines; for most output streams prefer Flow. actor serializes state via messages, but today a Mutex or a StateFlow is usually simpler and stable. Use the builders when the channel semantics genuinely fit.

solid answer

~40 s

`produce` is `@ExperimentalCoroutinesApi` and `actor` is `@ObsoleteCoroutinesApi`, so I weigh stability. For producing values, **`Flow`** (cold, backpressure-friendly, rich operators, structured) is the default; I reach for `produce` only when I need a **hot** channel with fan-out/fan-in pipeline semantics or to bridge callback/push sources where `channelFlow`/`callbackFlow` don't fit cleanly. For serialized state, the actor pattern is sound, but I'd usually replace it with a **`Mutex.withLock`** guarding the state, or model state as a **`StateFlow`/`MutableStateFlow`** updated via `update { }` (lock-free CAS) — both are stable and more idiomatic. I keep `actor` when I need strict message ordering with heterogeneous commands and request/response via `CompletableDeferred`, and the queue/backpressure of a channel is the right tool. The decision is: stability, hot vs cold, and whether the semantics are streaming, state-guarding, or command-queue.

code

kotlin · 7 lines
kotlin
// Actor replaced by StateFlow for an observable counter
val counter = MutableStateFlow(0)
fun increment() = counter.update { it + 1 }   // lock-free, stable API
// consumers: counter.collect { render(it) }

// produce replaced by Flow for a value stream
fun primes(): Flow<Int> = flow { /* emit(...) */ }

go deeper

for a junior

Aware that Flow is the usual way to stream values and Mutex protects shared state.

for a middle

Can pick Flow vs produce and Mutex vs actor for concrete cases.

for a senior

Weighs hot vs cold, stability annotations, and StateFlow.update vs Mutex for state.

for a principal

Sets architectural policy: chooses primitives by stability/semantics, plans migration off obsolete APIs, and justifies the rare cases where the channel builders remain the right tool.

## Stability annotations matter - `produce` — `@ExperimentalCoroutinesApi`: usable, may evolve. - `actor` — `@ObsoleteCoroutinesApi`: the maintainers consider its design unsettled and discourage it for new code. For a long-lived production API I bias toward **stable** primitives unless the experimental one is clearly the best fit. ## Replacing `produce` with `Flow` `Flow<T>` is **cold** (work starts per collector), composable (`map`, `buffer`, `flatMapMerge`), backpressure-aware, and fully structured. It is the default for emitting a sequence of values. ```kotlin fun numbers(): Flow<Int> = flow { for (i in 1..5) emit(i) } ``` Use **`produce`** instead when you genuinely need a **hot** source shared across consumers, classic CSP pipeline stages (each stage a `produce` feeding the next), or fan-out. When bridging push/callback APIs, prefer **`callbackFlow`/`channelFlow`**, which give Flow ergonomics over an internal channel. ## Replacing `actor` for state Two idiomatic, stable options: 1. **`Mutex`** — guard the state directly: ```kotlin val mutex = Mutex(); var counter = 0 suspend fun inc() = mutex.withLock { counter++ } ``` Simpler than an actor when there's no message protocol, just mutual exclusion. 2. **`StateFlow`** — model state as an observable value updated with lock-free CAS: ```kotlin val state = MutableStateFlow(0) fun inc() = state.update { it + 1 } // atomic, no suspension ``` `update { }` retries on conflict; consumers observe via `collect`. Great when other code must **react** to state changes. ## When the actor still wins - A **command protocol**: heterogeneous messages (sealed class), strict FIFO ordering, and request/response via `CompletableDeferred`. - Natural **backpressure/queueing** of commands (channel capacity). - Wanting all mutations funneled through one mailbox for auditing/serialization. ## Decision checklist - Output stream of values? -> `Flow` (cold) unless you need hot/pipeline -> `produce`. - Bridge a callback/push API? -> `callbackFlow`/`channelFlow`. - Mutual exclusion over state? -> `Mutex.withLock`. - Observable evolving state? -> `StateFlow` + `update`. - Ordered command queue with replies? -> the actor pattern (accept the obsolete annotation, or hand-roll a `Channel` + `launch`).

  • Why is StateFlow.update often preferable to a Mutex for a simple counter?
    update uses a lock-free compare-and-set retry loop and exposes the value as an observable Flow, so it is non-suspending and other code can react to changes without polling.
  • What does callbackFlow give you over a raw produce channel?
    Flow operators, structured cancellation via awaitClose, and backpressure handling, while still bridging a callback-based source through an internal channel.

saying these in an interview costs you the question

  • Defaulting to produce/actor for everything despite their experimental/obsolete status
  • Using an actor where a one-line Mutex.withLock suffices
  • Claiming Flow can't do hot streams at all (SharedFlow/StateFlow exist)
  • Reaching for actor when reactive observation (StateFlow) is the real need
  • Not knowing update {} is lock-free CAS

context