Syncing multiple asynchronous requests in Java
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
When several asynchronous requests must finish before the next step can run, the core problem is coordination. In modern Java, the cleanest tool for this is usually CompletableFuture, because it lets you start requests concurrently, wait for all of them, and combine their results without manually managing thread joins. The goal is to synchronize completion, not to turn asynchronous code back into blocking code too early.
Start Independent Requests Concurrently
Suppose you need data from three services before building a response. Start them independently with supplyAsync.
Each task begins running without waiting for the others.
Wait for All Requests with allOf
Use CompletableFuture.allOf when all requests must complete before you proceed.
allOf completes when every supplied future finishes. After that, calling join() on the individual futures is safe because they are already done.
Combine Smaller Groups with thenCombine
If you only need to merge two results at a time, thenCombine can be clearer than a separate allOf call.
This reads naturally when one result depends on combining exactly two asynchronous values.
Handle Failures Explicitly
Synchronization is incomplete without error handling. If one future fails, allOf completes exceptionally.
You can recover per request or let the combined result fail and handle it once at the end.
Which strategy is right depends on whether partial results are acceptable.
Avoid Blocking Too Early
A common mistake is to start an asynchronous request and immediately call get() or join() on it. That defeats the point of running requests concurrently.
Bad pattern:
That sequence blocks after each call instead of allowing the requests to overlap.
Start them first, then synchronize later.
Use the Right Executor
Real asynchronous requests should usually run on an executor sized for the work they do. If every request blocks on network I/O, a tiny executor can become the bottleneck even when the coordination logic is correct.
Separating compute-heavy tasks from I/O-heavy tasks also makes failures and latency easier to reason about.
Common Pitfalls
- Calling
join()immediately after creating each future and losing concurrency. - Using raw threads when
CompletableFuturecomposition would be simpler. - Ignoring exceptions until a combined future fails unexpectedly.
- Using
allOfand then forgetting to read the individual results. - Running blocking I/O on an executor that was meant for lightweight tasks.
Summary
- Start independent asynchronous requests before waiting on any of them.
- Use
CompletableFuture.allOfwhen all results are required. - Use
thenCombinefor simple pairwise combinations. - Decide explicitly how failures should affect the combined outcome.
- Delay blocking until the point where synchronization is actually needed.
Related reading
- Syntax for an async arrow function
- SyntaxError Unexpected reserved word await, node.js is correct version
- System.AggregateException on Socket.EndAccept with TaskFactory.FromAsync
- System.Threading.Tasks.Task Method Not Found
- Syntax for creating a two-dimensional array in Java
- System.currentTimeMillis() vs. new Date() vs. Calendar.getInstance().getTime()
- System.Threading.Timer in C it seems to be not working. It runs very fast every 3 second
- System.Timers.Timer vs System.Threading.Timer

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.