What does supplyAsync() method do and when to use it
Structured Java interview answer with junior, middle, and senior-level explanation.
🟢 Junior Level
supplyAsync() — creates a CompletableFuture that executes a task asynchronously and returns a result.
// supplyAsync — task with return value
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
// Runs in another thread
return "Hello from async";
});
// Get the result
cf.thenAccept(result -> System.out.println(result));
Difference from runAsync():
supplyAsync()— returns a value (Supplier<T>)runAsync()— does not return (Runnable)
// supplyAsync — with result
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> "result");
// runAsync — no result
CompletableFuture<Void> cf2 = CompletableFuture.runAsync(() -> {
System.out.println("Done");
});
🟡 Middle Level
When to use
supplyAsync:
// When you need a result
CompletableFuture<User> user = CompletableFuture.supplyAsync(() -> {
return userRepository.findById(userId);
});
CompletableFuture<String> data = CompletableFuture.supplyAsync(() -> {
return httpClient.get(url);
});
With a custom Executor:
ExecutorService ioExecutor = Executors.newFixedThreadPool(10);
CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> {
return httpClient.get(url);
}, ioExecutor);
Typical mistakes
- Blocking in commonPool: ```java // ❌ Blocks ForkJoinPool CompletableFuture.supplyAsync(() -> { return httpClient.get(url); // blocking I/O });
// ✅ Custom Executor CompletableFuture.supplyAsync(() -> httpClient.get(url), ioExecutor);
### When NOT to use supplyAsync
- **Simple synchronous operations** — overhead of creating CF is not justified
- **Strict execution order** — use sequential calls
- **Need cancellation with interruption** — better to use FutureTask
---
## 🔴 Senior Level
### Internal Implementation
```java
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(asyncPool, supplier);
}
public static <U> CompletableFuture<U> supplyAsync(
Supplier<U> supplier, Executor executor
) {
return asyncSupplyStage(screenExecutor(executor), supplier);
}
// asyncPool = ForkJoinPool.commonPool()
// Creates a CompletableFuture and schedules the task in the executor
// Simplified pseudocode representation. Actual JDK method names differ.
Architectural Trade-offs
| supplyAsync | runAsync |
|---|---|
| Returns T | Returns Void |
| Supplier |
Runnable |
| For computations | For side effects |
Edge Cases
1. Exception in supplier:
CompletableFuture.supplyAsync(() -> {
throw new RuntimeException("Error");
}).exceptionally(ex -> "recovered");
2. Long-running tasks:
// ❌ Long task in commonPool
CompletableFuture.supplyAsync(() -> {
Thread.sleep(60000); // blocks for 60 seconds
return result;
});
// ✅ Custom Executor
CompletableFuture.supplyAsync(longTask, longTaskExecutor);
Performance
supplyAsync():
- Creation: ~10 ns
- Scheduling: ~1μs
- Thread switch: ~1-5μs
commonPool overhead: minimal
Custom executor: + queue overhead
Production Experience
@Service
public class OrderService {
private final ExecutorService ioExecutor;
public CompletableFuture<Order> getOrderAsync(Long id) {
return CompletableFuture.supplyAsync(() -> {
return orderRepository.findById(id);
}, ioExecutor);
}
// Combining
public CompletableFuture<OrderSummary> getSummaryAsync(Long id) {
CompletableFuture<Order> order = getOrderAsync(id);
CompletableFuture<User> user = getUserAsync(id);
return order.thenCombine(user, (o, u) ->
new OrderSummary(o, u)
);
}
}
Best Practices
// ✅ supplyAsync for computations
CompletableFuture.supplyAsync(() -> calculate());
// ✅ Custom Executor for I/O
CompletableFuture.supplyAsync(ioTask, ioExecutor);
// ✅ Virtual Threads (Java 21+)
CompletableFuture.supplyAsync(task, vThreads);
// ❌ Blocking in commonPool
// ❌ Long tasks without a custom Executor
🎯 Interview Cheat Sheet
Must know:
- supplyAsync(Supplier
) — async task that returns a result - runAsync(Runnable) — async task without a return value (Void)
- Without Executor: ForkJoinPool.commonPool(), with Executor: specified pool
- For I/O, always pass your own Executor
- supplyAsync for computations and fetch operations, runAsync for side effects
Common follow-up questions:
- supplyAsync vs runAsync — when to use which? — supplyAsync when you need a result (Supplier
), runAsync when you don't (Runnable) - supplyAsync without Executor — which pool? — ForkJoinPool.commonPool(), for I/O this leads to thread pool starvation
- Exception in supplier — what happens? — CF completes with CompletionException, need exceptionally/handle
- supplyAsync vs thenApplyAsync? — supplyAsync creates a new CF, thenApplyAsync transforms an existing one
Red flags (DO NOT say):
- “supplyAsync and runAsync are the same thing” — supplyAsync returns T, runAsync returns Void
- “supplyAsync without Executor is safe for HTTP” — blocking I/O in commonPool → starvation
- “supplyAsync creates a new thread” — it uses ForkJoinPool.commonPool() or the specified Executor
Related topics:
- [[12. What thread pool is used by default for async methods]]
- [[13. How to specify a custom Executor for CompletableFuture]]
- [[5. What do thenAccept() and thenRun() methods do]]
- [[15. Why is it important to avoid blocking operations in CompletableFuture]]