What are the design limitations of java.util.concurrent.Future, and how do CompletableFuture and structured concurrency address them?
answer
- Future: only block (get) or poll (isDone) — no callbacks/compose/combine
- Errors are pull-based: surface only on get()
- CompletableFuture = Future + CompletionStage: thenApply/Compose/Combine, allOf/anyOf, exceptionally
- Gap: no shared lifetime/cancellation for a group
- StructuredTaskScope (21+): scoped forks, sibling cancel, join as a unit; pairs with virtual threads
basics
~20 sPlain Future only lets you block on get() or poll isDone() — you can't attach a callback, chain steps, or combine several Futures without blocking a thread. CompletableFuture adds non-blocking composition and callbacks; structured concurrency manages a whole group of tasks with shared cancellation and scope.
solid answer
~50 sFuture is intentionally minimal: you can submit, then either block on get() or busy-poll isDone(). It offers no way to register a completion callback, no way to chain 'when this finishes, do that' without a blocking thread, no built-in combination of multiple Futures (waiting for all/any), and exceptions only surface lazily when you call get(). That forces blocking, thread-per-wait designs. CompletableFuture (Java 8) implements Future plus CompletionStage: thenApply/thenCompose/thenCombine chain transformations, allOf/anyOf combine many, whenComplete/exceptionally handle errors — all non-blocking and composable, completing the pipeline without parking threads. Its gap is lifecycle: a group of related CompletableFutures has no shared scope or cancellation. Structured concurrency (StructuredTaskScope, Java 21+ preview) closes that: tasks forked in a scope share a lifetime, errors/cancellation propagate to siblings, and the scope joins them as a unit — making concurrent code read like sequential code with reliable cleanup.
go deeper
Aware that Future only lets you block on get() and that newer APIs (CompletableFuture) can do more.
Lists Future's gaps (no callback/compose/combine) and names CompletableFuture's thenApply/thenCompose/allOf as the fixes.
Explains push- vs pull-based completion, why thread-per-wait scales poorly, and maps CompletableFuture operators to composition/combination/error-handling needs.
Compares all three models for an architecture, factoring virtual threads and structured concurrency's scoped cancellation/lifetime, and chooses per use case with trade-offs (allocation, readability, cancellation guarantees).
## What plain Future gives you — and what it doesn't `Future<V>` (Java 5) is deliberately small. Its only operations are: - `get()` / `get(timeout, unit)` — **block** waiting for the result. - `isDone()` / `isCancelled()` — **poll** state. - `cancel(mayInterruptIfRunning)`. From this small surface flow several **limitations**: 1. **No callbacks.** You cannot say 'run this code *when* the result is ready'. Your only options are to block a thread on `get()` or to repeatedly poll `isDone()` (busy-waiting). Both waste a thread per pending result. 2. **No composition/chaining.** There's no 'take this Future's result, transform it, producing another Future' without blocking to extract the value first. 3. **No combination.** Waiting for *all* of several Futures, or the *first* of them, must be hand-coded (e.g. a loop of `get()`s, or a `CompletionService`). 4. **Lazy, pull-based error delivery.** A task's exception sits dormant until *someone* calls `get()`; if nobody does, the failure can be silently lost. 5. **Encourages thread-per-blocking-wait designs**, which scale poorly: a server handling many concurrent operations ties up a pool thread for each outstanding `get()`. ## CompletableFuture (Java 8) — push-based composition `CompletableFuture<V>` implements `Future<V>` **and** `CompletionStage<V>`, adding a fluent, non-blocking API: ```java CompletableFuture .supplyAsync(() -> fetchUser(id), pool) // async produce .thenApply(User::email) // transform result .thenCompose(email -> lookupAsync(email)) // chain another async stage .thenCombine(otherFuture, (a, b) -> merge(a,b)) // combine two .exceptionally(ex -> fallback(ex)) // handle failure inline .whenComplete((res, ex) -> log(res, ex)); // callback on completion ``` Key additions: - **Callbacks** (`thenApply`, `thenAccept`, `whenComplete`) run *when* the stage completes — no blocking. - **Chaining** (`thenCompose` for dependent async steps). - **Combination** (`thenCombine` for two; `allOf`/`anyOf` for many). - **Inline error handling** (`exceptionally`, `handle`) — errors propagate **down the chain** automatically, push-based, rather than waiting for a `get()`. - **Manual completion** (`complete`, `completeExceptionally`) — you can fulfil it from any event source, decoupling it from executors. This turns 'block and wait' into 'declare a pipeline'; threads aren't parked just to wait. **What CompletableFuture still lacks:** a notion of a **group of related tasks with a shared lifetime**. If you fan out 10 CompletableFutures and one fails, the others keep running; there's no automatic sibling cancellation or scoped join, and leaked/forgotten stages are easy. ## Structured concurrency (StructuredTaskScope, Java 21+, preview) Structured concurrency applies the principle that **concurrent subtasks should have a lifetime bounded by a lexical scope**, just like a block bounds local variables. `StructuredTaskScope` lets you fork subtasks and then `join()` them as a unit: ```java try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Subtask<User> user = scope.fork(() -> fetchUser(id)); Subtask<Order> order = scope.fork(() -> fetchOrder(id)); scope.join(); // wait for all forks scope.throwIfFailed(); // propagate the first failure return combine(user.get(), order.get()); } // scope closes -> any still-running forks are cancelled ``` What it adds over CompletableFuture: - **Shared lifetime & scope:** all forks live and die within the try-with-resources block; nothing leaks past it. - **Error/cancellation propagation:** policies like `ShutdownOnFailure` cancel siblings as soon as one fails; `ShutdownOnSuccess` (first-good-answer) cancels the rest once one succeeds. - **Readability:** concurrent code reads top-to-bottom like sequential code, with reliable cleanup and clear ownership. - **Pairs naturally with virtual threads (Java 21)**, where blocking a (cheap) virtual thread is fine — so the 'thread-per-wait' cost that motivated CompletableFuture's non-blocking style is largely removed, and simple blocking code becomes attractive again. ## How to choose (architect's view) - **Plain Future / CompletionService:** a simple batch of independent tasks where you just need the results, possibly in completion order. Lightweight, allocation-cheap. - **CompletableFuture:** asynchronous *pipelines* — dependent transformations, fan-in/fan-out combination, callback-driven flows, integration with event sources. Best when you must avoid blocking platform threads. - **Structured concurrency + virtual threads:** when you want **grouped** subtasks with reliable joint cancellation, propagation, and sequential-looking code, and can target Java 21+. The through-line: `Future` solved *'get a value later'*; `CompletableFuture` solved *'compose async steps without blocking'*; structured concurrency solves *'manage a family of concurrent tasks as one unit with safe lifetimes'*.
- Why does plain Future encourage thread-per-blocking-wait designs, and why is that a scaling problem?Because the only way to consume a result is to block a thread on get() (or busy-poll). Each pending result thus ties up a platform thread; with many concurrent operations you exhaust the pool. CompletableFuture (non-blocking callbacks) or virtual threads (cheap to block) both relieve this.
- What does StructuredTaskScope add that a set of independent CompletableFutures does not?A shared, lexically-bounded lifetime: forked subtasks are joined as a unit and, via policies like ShutdownOnFailure/ShutdownOnSuccess, a failure or first success automatically cancels the siblings — with guaranteed cleanup when the scope closes. Independent CompletableFutures have no such shared scope or joint cancellation.
saying these in an interview costs you the question
- Claiming plain Future supports callbacks or chaining (it doesn't — that's CompletableFuture).
- Saying CompletableFuture's get() is the normal way to consume it — the point is non-blocking callbacks.
- Thinking CompletableFuture gives grouped cancellation of related tasks.
- Confusing structured concurrency with just 'using a thread pool'.
- Assuming virtual threads make CompletableFuture obsolete in all cases — pipelines/combination still benefit.