Dispatchers & Threading Interview Questions
KOTLIN › Coroutines
A teammate has wrapped every function in the repository layer in withContext(Dispatchers.IO), including ones that only call other suspend functions. What would you say in review?
What a strong answer covers: I would push back, because it treats main-safety as a ritual rather than a property. Main-safety means a function does not block its caller thread; a function whose body only awaits other suspend functions already satisfies that, so the switch buys nothing and costs a real dispatcher round trip each call. The right place for the switch is the innermost point where actual blocking happens: the DAO call, the file read, the synchronous parse. Push it in, not out. The tell that this is cargo cult rather than reasoning is that it is applied uniformly, which means nobody asked per function what blocks. I would also point out the loop case, because it is where the cost stops being theoretical: withContext inside forEach over a thousand rows pays a thousand round trips instead of one.
You have a Room database that starts throwing timeouts under load because too many coroutines are writing at once. How would you bound it?
What a strong answer covers: Dispatchers.IO.limitedParallelism(n) with n matched to what the database can absorb, exposed as the single dispatcher every write goes through. It creates no threads: it borrows from the pool IO already owns and simply refuses to dispatch more than n concurrently, so the excess work queues instead of piling into the driver. The older answer, a private newFixedThreadPool wrapped in asCoroutineDispatcher, does the same job but leaves you owning real threads you have to remember to close, and they sit idle rather than being available to the rest of the app. I would also check whether the real fix is upstream: if the writes are individually tiny, batching them into one transaction inside a single withContext is better than throttling a thousand separate ones.
Explain why Dispatchers.IO can run 64 threads on an 8-core phone when Dispatchers.Default is capped at 8, given they share the same pool.
What a strong answer covers: Because the limit exists for different reasons in each case. Default is capped at the core count because CPU-bound work genuinely cannot go faster than the cores available; more concurrent computation only adds context switching and cache pressure. IO work spends nearly all its wall-clock time blocked on a socket or a disk read, consuming no CPU, so capping it at the core count would throttle throughput for no benefit at all. They share one pool because a thread is a thread: IO is permitted to grow the shared pool past Default limit up to 64. Two consequences worth naming: switching between IO and Default is cheap because there is often no actual thread handoff, and the 64 is an application-wide ceiling rather than per call site, so an unbounded number of blocking calls will queue rather than spawn threads.
How would you make a mutable in-memory cache safe for concurrent access without using a lock?
What a strong answer covers: Confine it. Create a private dispatcher with limitedParallelism(1) and route every read and write through withContext on it. That serialises execution so no two coroutines touch the state at once, and the dispatch that hands work between threads provides the happens-before edge, so one coroutine writes are visible to the next. The underlying collection can be a plain mutableMapOf with no synchronisation at all. The trap is a leaky boundary: if a getter returns the internal map directly rather than a copy, a caller can mutate it from any thread and the confinement is gone, so accessors have to return snapshots. I would weigh it against the alternatives: a ConcurrentHashMap if the operations are individually atomic and expressible with compute or merge, a MutableStateFlow of an immutable map with update if the cache is small and read-heavy, and a Mutex if the critical section needs to suspend, since that is the one thing confinement handles poorly because it serialises the whole slow operation.
What is the difference between Dispatchers.Main and Dispatchers.Main.immediate, and when does it actually matter?
What a strong answer covers: Plain Main always dispatches through Handler.post to the main Looper, even when the calling thread is already the main thread, so the block runs on a later pass of the message queue. Main.immediate checks the current thread first and executes inline when it is already correct. The difference is one frame of latency, which matters for UI state updates: if a StateFlow emits while you are already on the main thread, you want that render on this frame, not the next one. That is precisely why repeatOnLifecycle and collectAsStateWithLifecycle use immediate internally. It matters in the other direction too. If you genuinely want to yield to the message queue, for instance to let a pending layout pass complete before you measure something, plain Main is the correct choice and immediate would defeat it.
Someone suggests using Dispatchers.Unconfined to avoid the cost of dispatching. How do you respond?
What a strong answer covers: That it does avoid the dispatch, and that this is almost never worth what it costs you. Unconfined runs the coroutine body inline on the calling thread up to the first suspension point, and after that resumes on whatever thread happened to complete the suspending call, which for delay is a shared scheduler thread. So the thread your code runs on after any suspension is undefined. That rules it out for anything touching UI, and it makes reasoning about thread confinement impossible, since the same code can run on different threads on different passes. Where it is legitimate is testing, where UnconfinedTestDispatcher runs coroutines eagerly so you can assert without advancing a scheduler, and framework code that only forwards a value onward and genuinely does not care where it lands. In application code, the desire for inline execution nearly always means Main.immediate, which gives you the inline behaviour with a defined thread.