Use CompletableFuture on ForkJoinpool and avoid thread waiting
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
Using CompletableFuture with a ForkJoinPool works well only when the tasks stay genuinely asynchronous and non-blocking. The most common mistake is to submit work to the pool and then call get() or join() inside another task running on the same limited pool, which turns concurrency into thread waiting.
Why Waiting Inside the Pool Is a Problem
ForkJoinPool is designed for short compute-oriented tasks and work stealing. If worker threads spend their time blocked on other futures, the pool can lose throughput and in bad designs can even stall progress.
This is the pattern to avoid:
The combining task itself occupies a pool thread while waiting for other tasks. That is exactly the kind of avoidable blocking CompletableFuture is meant to remove.
Compose Futures Instead of Waiting
The normal fix is to chain futures rather than blocking on them.
Here no worker thread waits just to combine results. The combination runs when both inputs complete.
Use thenCompose for Dependent Async Steps
If the second step itself returns a future, use thenCompose instead of calling join() inside a callback.
This keeps the workflow asynchronous from end to end.
Gather Many Futures with allOf
For several independent tasks, allOf is often cleaner than a web of nested callbacks.
This is acceptable because the join() calls happen only after allOf has completed, so they are not blocking on unfinished work.
Use the Right Executor for the Workload
Another important design point is that ForkJoinPool is best for CPU-oriented tasks. If the work is blocking I/O such as database calls or HTTP requests, a dedicated executor is often a better choice.
Using the wrong executor is often the deeper bug behind "threads are waiting" complaints.
Keep Blocking at the Boundary
Sometimes you do need to wait for a result, but do it at the application boundary, not inside the internal async graph.
Examples of acceptable boundaries include:
- returning a final value from
main - adapting to an older synchronous API
- waiting in a top-level test method
Inside the future graph itself, prefer composition over blocking.
Common Pitfalls
- Calling
join()orget()inside tasks running on the sameForkJoinPool. - Using
supplyAsynceverywhere but still writing the workflow as if it were synchronous. - Running blocking I/O on a compute-oriented fork-join pool.
- Forgetting that
allOf(...).thenApply(...)is safer than waiting in an intermediate task. - Blocking deep inside the async pipeline instead of only at the outer boundary when necessary.
Summary
- '
CompletableFutureavoids waiting only if you compose futures instead of blocking on them.' - Use
thenCombine,thenCompose, andallOfto build non-blocking workflows. - Avoid
join()andget()inside fork-join worker tasks. - Use an executor suited to the actual workload, especially for blocking I/O.
- If blocking is unavoidable, keep it at the boundary of the system rather than inside the async graph.
Related reading
- Use of atomic properties in Objective C Any side effects?
- Use of await keyword in c
- use of CompletableFuture.supplyAsync and CompletableFuture.completedFuture within Async annotation in spring boot
- Use of return in deferred computation in Ocaml Async
- Use JAXB to create Object from XML String
- Use Keycloak Spring Adapter with Spring Boot 3
- Use Powershell's BeginInvoke and EndInvoke in async way
- Using a coroutine as decorator

OOD Fundamentals
Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.