Prompt and context
The interviewer gives you a request handler that runs three related operations in parallel: fetch a user profile, fetch orders, and generate recommendations. The client disconnects, a critical child fails, or the overall deadline expires. The system must not leave orphan work running after the request has ended. Explain structured concurrency, then map it to task groups, coroutine scopes, or structured task scopes without making the answer depend on one API.
This question fits mid-level and senior backend, platform, mobile, and cross-language infrastructure roles. The target is lifecycle reasoning: entry, exit, failure policy, and resource ownership.
What the interviewer is testing
The interviewer wants to know whether you treat concurrent work as part of a parent task instead of submitting work to a global pool and returning. A strong answer says that children do not outlive their scope, parent cancellation propagates, failures can be fail-fast or partial by business value, and joining and cleanup happen inside a visible boundary.
A weak answer says “use async/await in parallel.” A strong one distinguishes structured concurrency from detached futures and explains when work must become a separate durable queue with a new owner.
Questions to clarify first
- Must all three children succeed, or are profile and orders required while recommendations are optional?
- Is the deadline a hard request deadline, or can the server finish under a separate soft budget?
- Do children perform cancellable I/O only, or can they trigger external side effects that cannot be undone?
- After a client disconnects, may the work move to an asynchronous notification or background job?
- Should failure return one error, partial results, or an explicitly degraded response?
These answers change the scope boundary: short request work stays inside the parent scope; work that continues after the request needs an explicit ownership transfer.
A 30-second answer
I treat the request as the parent task and the three parallel reads as child tasks. The parent exits only after children finish, fail, or are cancelled and cleaned up. A profile or order failure cancels siblings and returns a retryable error; recommendation failure returns an explicit degraded response. Client disconnects and deadlines propagate cancellation downward, and each child closes connections and handles in its cleanup path. If work must continue after the request, I write it to a durable queue and start a new lifecycle instead of detaching a background future. I verify cancellation, timeout, partial failure, and leak metrics with fault injection.
Step-by-step answer
Step 1: Draw the task tree
Make the request handler the root. Profile, orders, and recommendations are direct children. Children inherit the parent deadline, tracing context, and cancellation signal. The parent owns waiting, error composition, and scope closure; a child cannot hand work to a global executor and let the parent return without an ownership transfer.
The key invariant is checkable: when the parent exits, every child has completed, been cancelled, or been explicitly transferred to another recorded owner. Without that invariant, thread leaks, duplicate writes, and unexplained tail latency stay hidden.
Step 2: Choose failure and partial-result policy
Classify by business value. Profile and orders are required for a checkout view, so either failure cancels siblings and returns a retryable error. Recommendations are an enhancement, so failure returns a response without recommendations and records the degradation. Do not let a low-value failure take down the request, and do not disguise missing critical data as an empty object.
Pseudocode can express the policy without choosing a language:
within request_scope(deadline):
profile = child(fetch_profile)
orders = child(fetch_orders)
recommendations = child(fetch_recommendations)
wait(profile, orders)
if profile.failed or orders.failed:
cancel_all_children()
return retryable_error
return compose(profile, orders, recommendations.or_empty)Step 3: Make cancellation reach resource boundaries
Cancellation is not just a boolean on a task object. Network clients, database drivers, and file reads must observe the signal; waits must be interruptible; retry loops must re-check the deadline before another attempt. For an irreversible side effect, such as an accepted payment or sent email, cancellation can stop later steps but cannot claim that the completed side effect was rolled back.
When the client disconnects, the entry point cancels the root. The root propagates cancellation, and children close response bodies, connections, subscriptions, and temporary files in their cleanup path. Cleanup itself needs a bound so that “waiting for cleanup” does not block request shutdown forever.
Step 4: Separate timeout, cancellation, and failure
A timeout means the budget was exhausted. Cancellation means an upstream no longer needs the result. Failure means the task could not complete. They can coincide, but logs and user state should not collapse into one 500. Record the original cause, trigger, and task path. Usually do not retry a client disconnect; allow one short retry after a dependency timeout only if the remaining budget permits it; send business errors through an explicit degradation policy.
Do not put blind retries or sleep outside the scope. They consume an expired budget and keep generating load after the parent has returned.
Step 5: Prove that structure is preserved
Test normal completion, critical-child failure, optional-child failure, parent cancellation, deadline expiry, and cancellation before a child starts. Every test checks that active children return to zero, downstream connections close, traces show parent-child relationships, side effects are not duplicated, and the error classification matches the user-visible state.
Track active tasks, cancellation propagation latency, deadline timeout rate, child exceptions, cleanup duration, tasks still running after cancellation, and maximum fan-out per request. A successful response alone does not prove structured concurrency.
Model high-quality answer
“I would treat one request as a parent task and put the three reads in one deadline-bound scope. The parent creates, waits for, and closes the children; no child may outlive that scope. Profile and orders are critical dependencies, so either failure cancels siblings and returns a retryable error. Recommendations are optional; on failure I return an explicit empty recommendation state and record the degradation.
Client disconnect, parent timeout, or upstream cancellation propagates down the task tree. Each I/O call uses a cancellable interface, retries check the remaining budget, and cleanup closes connections and subscriptions. I do not pretend an external side effect can be rolled back after it happened. If work must continue after the request, I first write it to a durable queue and let a consumer create a new task tree.
I would inject critical failure, optional failure, cancellation races, and cleanup timeouts, then observe active tasks, cancellation latency, leaks, and parent-child traces. The important part is lifecycle and ownership, not a particular Java, Kotlin, or Swift API.”
Common failure modes
- Equating structured concurrency with parallel execution → You only describe starting tasks together and omit lifetimes → Show the scope, join, cancellation, and cleanup boundaries.
- Submitting to a global executor and returning → Work can access request resources after the parent is gone → Keep it in the request scope or transfer it explicitly to a durable queue.
- Treating cancellation as a forced kill → External effects may already be complete and resources may not close automatically → State cooperative cancellation, irreversible work, and cleanup ownership.
- Returning an empty result for every failure → Missing critical data is hidden from callers → Define fail-fast and partial-result rules by business criticality.
- Testing only success → Cancellation races and orphan work remain invisible → Inject disconnects, deadlines, sibling failures, and repeated cancellation, then check that active work reaches zero.
Follow-up questions
Follow-up 1: Recommendation generation takes seconds, but the request has already ended. What do you do?
First ask whether the user still needs the result. If it is only for the current page, cancel it with the parent. If the business wants asynchronous generation, persist the input and an idempotency key, then let a consumer create a new scope. That consumer owns a new deadline, retry policy, and alerting; it cannot borrow the completed request context.
Follow-up 2: Why not let the other children finish after one child fails?
You can, if the remaining work has value and fits the budget. Continuing an order lookup after critical profile data has failed usually wastes connections and downstream capacity; finishing an independent cache refresh or audit record may be reasonable. State the condition for cancellation instead of treating fail-fast as universal.
Follow-up 3: The cancellation signal was sent, but the database query is still running. How do you handle it?
Check whether the driver supports cancellation and returns the connection. If it does not, apply an independent query timeout, isolate the pool, and protect against using a late result. Measure the time from cancellation to resource release; crossing a threshold should trigger degradation, isolation, or an alert. An uncancellable query must not hold request-level capacity indefinitely.
Follow-up 4: Does structured concurrency replace every thread pool and message queue?
No. It fits short-lived work with a clear parent-child relationship and shared cancellation and cleanup. Cross-request work, scheduled work, and durable retries still need a queue or workflow. A shared pool can provide execution resources, but the submitting scope must define who waits, who cancels, and who owns the result.