Thread Scheduling's Home: Java Hands It to the Caller, Kotlin Coroutines Tuck It Inside the API
Android apps that scatter thread-switching logic across ViewModels and Repositories become hard to maintain and test. Coroutines provide a way to push that responsibility into lower layers, letting higher layers stay clean and lifecycle-aware without losing readability.
Java code either annotates blocking methods with @WorkerThread and leaves the caller to arrange a thread pool and post results back to the main thread, or uses callbacks that still force the caller to wire together asynchronous flow. Both approaches make thread switching a visible part of upper-layer business logic.
Kotlin coroutines flip that. A suspend function can internally use withContext(Dispatchers.IO) to switch execution environments, and the caller just sees a suspension point that resumes on the original context. The article walks through a ViewModel-Repository-DataSource example where only the DataSource touches dispatchers, keeping the other layers clean and lifecycle-aware.
The key principle: whoever best knows an operation's execution requirements should be the one to satisfy them. Suspend functions should be Main-safe, and structured concurrency only works when cancellation signals are actually propagated.
Coroutines change the location of scheduling responsibility, not the existence of scheduling; someone still has to decide which dispatcher runs what.
Java's callback style forces the caller to orchestrate an asynchronous pipeline, while suspend functions let the API hide that orchestration—but hiding it makes main-safety a contractual requirement.
The three-layer example shows that even with coroutines, developers must consciously choose which layer performs the switching; nothing happens automatically.
Viewing coroutines as merely a nicer async syntax misses that they also create a lifecycle contract with the surrounding component, something callbacks or Executors do not provide by default.