Question 17 · Section 19

What does supplyAsync() method do and when to use it

Structured Java interview answer with junior, middle, and senior-level explanation.

Language versions: English Russian Ukrainian

🟢 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

  1. 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]]