Design and diagnose CompletionStage graphs with explicit execution, ownership, failure, timeout, cancellation, context and admission semantics. Use when a continuation runs on an I/O thread, a branch failure disappears, allOf or anyOf has the wrong policy, a timeout leaves work running, or asynchronous fan-out overloads a dependency. Distinguishes Java 17/21 APIs from Java 25 preview structured-concurrency alternatives.
Scanned 9/19/2026
Install to Claude Code
npx -y skills add robsonkades/agent-skills --skill completablefuture-composition --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Completablefuture Composition?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/robsonkades-completablefuture-composition)More formats (shields.io, HTML) on the badges page.
---
name: completablefuture-composition
description: >
Design and diagnose CompletionStage graphs with explicit execution, ownership, failure,
timeout, cancellation, context and admission semantics. Use when a continuation runs on an
I/O thread, a branch failure disappears, allOf or anyOf has the wrong policy, a timeout leaves
work running, or asynchronous fan-out overloads a dependency. Distinguishes Java 17/21 APIs
from Java 25 preview structured-concurrency alternatives.
---
# CompletableFuture Composition
## Purpose
Treat a `CompletableFuture` as a graph of completion dependencies, not as a lighter thread. The
graph is correct only when every edge has an execution policy, every branch has an outcome owner,
and the caller's deadline is connected to the underlying operation.
This skill owns stage composition. Cancellation mechanics belong to
`cancellation-and-interruption`; worker scheduling to `forkjoinpool-and-work-stealing`; lexical
fork/join lifetimes to `structured-concurrency`.
## Investigation workflow
The composition snippets use Java 17 final APIs unless labelled otherwise; ordinary virtual
threads require Java 21+, and the Java 25 structured-concurrency alternative is preview.
Inspect compiler release/toolchains, deployed JDK, resolved client/context libraries and actual
executor configuration before applying a recipe. Do not upgrade Java, enable preview or add
dependencies just to match a snippet. If graph ownership or client completion semantics are
missing, request that evidence and keep the proposed fix conditional.
1. Draw nodes and edges: creation, completion, dependent stages, fan-out and terminal observation.
2. For each action, identify the executor allowed to run it. Do not infer a thread from a method
name or a test run.
3. State outcome policy: propagate, recover, observe, aggregate, race, or tolerate partial failure.
4. Separate caller timeout, operation timeout and cancellation. They are different mechanisms.
5. Bound admission at the scarce resource, not merely the number of executor workers.
6. Capture and restore observability/security context explicitly.
7. Test adversarial orderings: inputs already complete, simultaneous failures, rejection, timeout,
cancellation and callback duplication.
## Execution contracts
| Form | Contract | Engineering consequence |
| ---------------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| non-`Async` dependent method | action may run in the completing thread or another caller of a completion method | it may execute inline on an event loop, request thread, test thread, or application completer |
| `*Async` without executor | uses the stage's default async facility | base Java 17/21 futures use a thread-per-task fallback at low common-pool parallelism; OpenJDK 25 uses the common pool. Subclasses may override `defaultExecutor()` |
| `*Async(..., executor)` | normally submits through the supplied executor | rejection, queuing and actual concurrency depend on the executor; Java 17/21 can substitute the fallback even for an explicitly supplied common pool at low parallelism |
Use a non-`Async` stage only for small, non-blocking, non-reentrant transformations that are safe on
any completing thread. Use an explicit executor when isolation, context, blocking behavior or
capacity matters. An explicit executor does not by itself promise a new thread or concurrent
execution.
Do not derive the common pool from `availableProcessors() - 1` as an invariant. Active processor
count, common-pool properties, embedding and implementation version can change it. Record effective
pool configuration in the environment being diagnosed.
## Composition and outcome semantics
- `thenApply` maps `T -> U`; `thenCompose` flattens `T -> CompletionStage<U>`.
- A dependent stage that requires normal completion is skipped when its source fails and propagates
exceptional completion, commonly represented by a `CompletionException`.
- `exceptionally` recovers from failure with a value; `exceptionallyCompose` recovers with another
stage; `handle` maps either outcome; `whenComplete` observes either outcome.
- `whenComplete` is not outcome-neutral if its action throws. It replaces a successful source with
its own failure; when both source and action fail, the source failure has precedence.
- A handler's `Throwable` is not guaranteed to be either wrapped or unwrapped. Direct
`completeExceptionally(x)`, dependent propagation, `join()` and `get()` have different surfaces.
Normalize only known transport wrappers at the boundary where type-based policy is needed.
- `allOf` completes after all supplied futures complete, returns `Void`, and is not a fail-fast
cancellation policy. Joining the inputs after successful `allOf` cannot block, but results and
failure aggregation remain application policy.
- `anyOf` exposes the first normal or exceptional completion as `Object`. It is neither typed nor
“first success,” and it does not cancel losers.
- Empty inputs matter: `allOf()` is already normally complete; `anyOf()` remains incomplete.
- Do not block a continuation by joining an unfinished sibling on the same bounded executor
or completer thread. Express that dependency with `thenCombine`/`thenCompose`; completion
ordering and sibling registration order are not scheduling guarantees.
If several inputs fail, do not promise which cause an aggregate exposes unless the application
records each branch itself. If partial results matter, turn each branch into an explicit success or
failure value rather than erasing errors into `Optional.empty()`.
## Time, cancellation and ownership
`orTimeout` and `completeOnTimeout` mutate and return the same `CompletableFuture`. They race to
complete that representation; they do not establish a network, JDBC or file-system deadline and do
not stop the supplier. A late operation may still consume a connection and produce side effects
after callers have degraded.
For each operation define:
- the end-to-end deadline and remaining budget passed downstream;
- the provider-specific connect/read/request/query timeout;
- whether cancellation is supported and whether it is best effort;
- whether a late success is harmless, deduplicated, compensated or an unknown outcome;
- which component observes terminal failure and records it exactly once.
Base `CompletableFuture.cancel(true)` does not interrupt its supplier. Provider futures can add
cancellation behavior: Java 25's default `HttpClient` returns cancelable futures, including derived
ones. Inspect the actual handle; `copy()` is not a universal cancellation-isolation boundary.
Cancellation return value, public exceptional state, actual resource release and remote effects
are separate observations; use `cancellation-and-interruption` for the provider contract.
## Fan-out and admission
Creating 100,000 futures creates 100,000 graph nodes even when only 32 workers execute. A bounded
worker pool limits simultaneous worker execution but can still accept an enormous queue; an
unbounded virtual-thread executor limits neither admission nor dependency pressure.
Choose the bound from the protected resource: downstream quota, connection pool, partition, memory
budget or latency SLO. Acquire interruptibly before launching the scarce operation, release in
`finally` for synchronous work, and define what happens when admission exceeds its budget.
For asynchronous clients, release only when the actual protected operation/resource is terminal,
not when submission returns a stage or a caller-facing timeout completes. Do not perform a
blocking permit acquisition on an event loop; use bounded queuing, nonblocking rejection or an
appropriate submitting worker. Batch/windowed production can
bound both live graph size and downstream concurrency.
## Context and security
Thread-local MDC, tracing state, authentication context and transaction state do not automatically
follow arbitrary completion edges. Capture only safe immutable context, restore it around the
action, and close/clear it in `finally` because pool threads are reused. Prefer the instrumentation
library's executor/stage wrapper over a bespoke copier. Never propagate a transaction/session object
to concurrent stages unless its API explicitly permits concurrent use.
## Decision guide
Prefer `CompletableFuture` when:
- adapting a callback API with exactly-once completion control;
- publishing an API whose contract already is `CompletionStage`;
- representing a genuinely dynamic, non-lexical dependency graph.
Prefer ordinary blocking code on virtual threads when the graph only compensates for expensive
platform threads and lexical control flow improves cancellation and debugging. For Java 25,
`StructuredTaskScope` joiners can express all-success or first-success policies, but the API is
preview and requires preview enablement; do not present it as a Java 21 production-stable API.
Avoid a stage graph when nobody can identify the owner that waits, observes failure and ends the
lifetime of every branch.
## Production signals
Measure accepted/rejected work, live graph count, stage latency by operation, executor queue and
active workers, timeout/cancellation/late-completion counts, dependency concurrency, and terminal
failure by cause. Executor metrics are snapshots and graph-dependent counts are diagnostic estimates,
not a linearizable accounting system.
### Symptom-driven diagnosis
| Symptom | Distinguish with | Likely remediation |
| -------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------- |
| event loop stalls | thread dump plus stage/executor attribution | move blocking or expensive continuation to an explicit executor |
| caller timed out but load keeps rising | compare caller timeout with client active requests/connections | enforce provider deadline and bound admission |
| fallback did not match | inspect the actual cause chain and completion path | unwrap only known wrappers; test direct and dependent failure |
| aggregate waits after an early failure | inspect `allOf` policy and remaining branches | use explicit short-circuit/cancellation ownership where required |
| memory rises during fan-out | count live futures, queued tasks and input cardinality | process bounded windows rather than constructing the whole graph |
| missing trace/user context | compare captured context at submission and execution | use supported context propagation and guaranteed cleanup |
## Review checklist
- [ ] Every `*Async` edge has an intentional executor or a documented safe default.
- [ ] Every non-`Async` action is safe inline on any completer.
- [ ] Every branch has a terminal observer and outcome policy.
- [ ] Deadlines reach the underlying client; timeouts are not mistaken for cancellation.
- [ ] Admission protects the actual scarce resource and bounds live graph size.
- [ ] Empty input, null result, rejection, duplicate callback and simultaneous failure are tested.
- [ ] Context is restored and cleared; mutable sessions/transactions are not shared unsafely.
- [ ] Version-dependent preview alternatives are labelled.
Report the relevant graph edge, execution/ownership contract, observed outcome and proposed
change with its deterministic ordering/resource test. Mark untested cancellation or latency
guarantees explicitly instead of equating future completion with work termination.
## References
- [Composition recipes](references/composition-recipes.md) — read when implementing branch
aggregation, permit ownership, caller deadlines or a callback adapter.
- [Executors, failures and context](references/pitfalls-and-executors.md) — read when diagnosing
thread affinity, exception surfaces, observer failure or lost/leaked context.
- [Java 25 `CompletableFuture` API](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/CompletableFuture.html)
- [Java 25 `CompletionStage` API](https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/concurrent/CompletionStage.html)
- [JEP 444: Virtual Threads](https://openjdk.org/jeps/444)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!