Structured Concurrency

📖 Concept

Structured concurrency is the principle that a coroutine's lifetime is bounded by its scope. When a scope is cancelled, all its child coroutines are cancelled. When a child fails, the parent is notified.

Why it matters: Without structured concurrency (like raw threads), you get:

  • Leaked threads that run after the component is destroyed
  • No automatic cleanup on errors
  • Manual lifecycle management for every async operation

The job hierarchy:

viewModelScope.launch (parent Job)
  ├── async { fetchUser() }      (child Job 1)
  ├── async { fetchPosts() }     (child Job 2)
  └── launch { logAnalytics() }  (child Job 3)

If viewModelScope is cancelled → all three children are cancelled. If child Job 1 fails → siblings 2 and 3 are cancelled (in regular scope), parent is cancelled.

Cancellation propagation rules:

  1. Parent cancellation → All children cancelled
  2. Child failure → Parent cancelled → All siblings cancelled (coroutineScope)
  3. Child failure → Only failed child cancelled, others continue (supervisorScope)

Cooperative cancellation: Coroutines must cooperate by checking isActive or using cancellable functions. delay(), yield(), and all kotlinx.coroutines functions check for cancellation automatically. CPU-bound loops must check manually with ensureActive().

💻 Code Example

codeTap to expand ⛶
1// Structured concurrency in action
2class DataSyncManager(
3 private val scope: CoroutineScope // Injected scope with lifecycle
4) {
5 private var syncJob: Job? = null
6
7 fun startSync() {
8 syncJob?.cancel() // Cancel previous sync
9 syncJob = scope.launch {
10 // All children are bounded by this scope
11
12 // Parallel but dependent — if one fails, cancel all
13 coroutineScope {
14 launch { syncUsers() }
15 launch { syncPosts() }
16 launch { syncComments() }
17 }
18 // Only reaches here if ALL three succeed
19 markSyncComplete()
20 }
21 }
22
23 fun stopSync() {
24 syncJob?.cancel() // Cancels everything cleanly
25 }
26}
27
28// Cooperative cancellation — CPU-bound work
29suspend fun processLargeList(items: List<Item>) {
30 for (item in items) {
31 ensureActive() // Check if cancelled before each iteration
32 processItem(item) // If scope cancelled, throws CancellationException
33 }
34}
35
36// NonCancellable — for cleanup that must complete
37suspend fun saveAndCleanup(data: Data) {
38 try {
39 saveToNetwork(data)
40 } finally {
41 // Even if cancelled, cleanup must run
42 withContext(NonCancellable) {
43 saveToLocalDb(data) // Won't be cancelled
44 closeConnections()
45 }
46 }
47}
48
49// Timeout with structured concurrency
50suspend fun fetchWithTimeout(): Result<Data> {
51 return try {
52 withTimeout(5000) { // Cancels after 5 seconds
53 val data = api.fetch()
54 Result.success(data)
55 }
56 } catch (e: TimeoutCancellationException) {
57 Result.failure(e) // Timeout is a specific CancellationException
58 }
59}
60
61// SupervisorScope — independent children
62suspend fun loadDashboard(): Dashboard {
63 return supervisorScope {
64 val weather = async {
65 try { api.getWeather() } catch (e: Exception) { null }
66 }
67 val news = async {
68 try { api.getNews() } catch (e: Exception) { emptyList() }
69 }
70 val stocks = async {
71 try { api.getStocks() } catch (e: Exception) { emptyList() }
72 }
73 // Each can fail independently — others continue
74 Dashboard(weather.await(), news.await(), stocks.await())
75 }
76}

🏋️ Practice Exercise

Practice:

  1. Explain the job hierarchy and how cancellation propagates through it
  2. Write a coroutine that processes a large list with cooperative cancellation
  3. Implement a retry mechanism using structured concurrency (max 3 retries, exponential backoff)
  4. Explain when to use NonCancellable and why it's important in finally blocks
  5. Design a data sync system that uses supervisorScope for independent sync operations

⚠️ Common Mistakes

  • Not checking for cancellation in CPU-bound loops — the coroutine keeps running even after the scope is cancelled

  • Using try-catch in finally without NonCancellable — suspend functions in finally block are cancelled immediately

  • Creating unstructured coroutines with GlobalScope in ViewModel — they outlive the ViewModel

  • Not understanding that delay() checks for cancellation — it throws CancellationException if the coroutine is cancelled

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Structured Concurrency. Login to unlock this feature.