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?
answer
- produce = ExperimentalCoroutinesApi, actor = ObsoleteCoroutinesApi
- values -> Flow (cold); hot/pipeline -> produce
- state mutex -> Mutex.withLock
- observable state -> StateFlow.update (CAS)
- command queue + replies -> keep actor pattern
basics
~20 sproduce 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// 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
Aware that Flow is the usual way to stream values and Mutex protects shared state.
Can pick Flow vs produce and Mutex vs actor for concrete cases.
Weighs hot vs cold, stability annotations, and StateFlow.update vs Mutex for state.
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