## Summary
A Kotlin suspend function mixing foreground and background work caused unexpected blocking behavior when called. The function contained an immediate task (set `a=2`) and a delayed background task (set `a=3`). When called synchronously (`bar()`'s `foo()` call), it blocked until **both tasks completed**, preventing immediate access to intermediate results (`a=2`).
## Root Cause
- **`coroutineScope` blocks until all child coroutines complete**. Using `coroutineScope` in `foo()` forced the caller to wait for `launch { ... }` (1-second delay), even though that work was intended to run asynchronously.
- **Poor separation of concerns**. Combining synchronous and fire-and-forget logic in a single suspend function created implicit dependency on background job completion.
## Why This Happens in Real Systems
- Engineers treat suspend functions as containing **fully asynchronous logic** by default
- **Concurrency lifecycle blindness**: Misjudging which tasks require wait semantics vs autonomous execution
- Overusing suspending contexts for mixed workflows without explicit control boundaries
- Tight coupling of logic phases due to shared state (`var a`)
## Real-World Impact
- **Blocked callers wasted resources** awaiting non-critical background work
- **Delayed processing** of intermediate results (e.g., UI updates with placeholder `a=2`)
- **Incorrect state visibility** causing race conditions when background work overrides state
- **Degraded throughput**: Synchronization hotspots where connectors shouldn't exist
## Example or Code
```kotlin
// Original problematic structure
suspend fun foo() = coroutineScope {
delay(100)
a = 2 // Should be synchronous
launch { // Should be asynchronous
delay(1000)
a = 3
}
}
// Fixed approach for bar()
suspend fun bar() = coroutineScope {
fooImmediate() // Synchronous part
launch {
fooAsync() // Background work
}
println(a) // Now correctly prints 2
}
// Refactored functions
suspend fun fooImmediate() {
delay(100)
a = 2
}
suspend fun fooAsync() {
delay(1000)
a = 3
}
How Senior Engineers Fix It
-
Decouple logical phases: Split
suspend fun foo()into:- A synchronous suspending function (
fooImmediate()) - An asynchronous fire-and-forget function (
fooAsync())
- A synchronous suspending function (
-
Encapsulate state transitions:
suspend fun executeWorkflow() { doImmediateWork() startBackgroundWork() // Returns immediately } -
Design API boundaries:
- Prefix with
start...for fire-and-forget methods - Use
run.../do...for synchronous tasks - Document background job ownership in method contracts
- Prefix with
-
Enable caller control: Let calling code manage the coroutine scope:
// Caller chooses when to wait vs launch suspend fun fooImmediate() = { ... } fun CoroutineScope.fooAsync() = launch { ... }
Why Juniors Miss It
- CoroutineScope misunderstanding: Assuming
launchinside suspend functions doesn’t block callers - Magic-box thinking: Treating suspend functions as black boxes without considering internal concurrency mechanics
- Premature encapsulation:
- Forcing unrelated logic into single functions to “avoid duplication”
- Over-prioritizing code reuse over execution semantics
- Visibilityškills gap: Not pesar-leveling structured concurrency rules (
coroutineScopewaits for children)