Definition
- supplyAsync(Supplier
) – starts an async task using aSupplierwhoseget()method returns T; the result is stored inCompletableFuture. - runAsync(Runnable) – starts an async task using a
Runnablewith no return value; returnsCompletableFuturewhereget()returns null. - Default pool – both use
ForkJoinPool.commonPool()unless a customExecutoris passed as the second parameter. - Custom executor – pass a dedicated pool for IO-bound tasks since commonPool is small and shared.
- Chaining – supplyAsync chains naturally with thenApply (transforms value); runAsync chains with thenRun (sequential void actions).
At National Bank of India: supplyAsync is "Go fetch Rajesh's account balance and bring it back to me." The teller returns with a number. runAsync is "Go send an SMS notification to Rajesh about his settled trade." No response expected, just do it in the background.
Understanding the Concepts
What is supplyAsync?
supplyAsync takes a Supplier and runs it asynchronously. The Supplier's get() method returns a value of type T, which is stored in the resulting CompletableFuture.
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class SupplyAsyncDemo {
public static void main(String[] args) {
CompletableFuture<BigDecimal> balanceCF = CompletableFuture
.supplyAsync(() -> {
// Simulate database query
return new BigDecimal("250000.50");
});
BigDecimal balance = balanceCF.join();
System.out.println("Balance: Rs." + balance);
}
}
Balance: Rs.250000.50
What is runAsync?
runAsync takes a Runnable and runs it asynchronously. The Runnable's run() method returns nothing, so the result is CompletableFuture where join() returns null.
package com.bank.service;
import java.util.concurrent.CompletableFuture;
public class RunAsyncDemo {
public static void main(String[] args) {
CompletableFuture<Void> notifyCF = CompletableFuture
.runAsync(() -> {
System.out.println("SMS sent to Rajesh: Trade TRD-001 settled");
});
notifyCF.join(); // returns null (Void)
System.out.println("Notification complete");
}
}
SMS sent to Rajesh: Trade TRD-001 settled
Notification complete
How do supplyAsync and runAsync compare?
| Feature | supplyAsync | runAsync |
|---|---|---|
| Input | Supplier
| Return | CF
| join() returns | T (the value) | null |
| Chaining | thenApply, thenCompose | thenRun, thenAccept |
| Use case | Need result | Fire-and-forget |
How do you use a custom executor?
Both methods accept a custom Executor as the second parameter. Use this for IO-bound work, since commonPool is small (cores - 1 threads) and shared.
package com.bank.service;
import java.util.concurrent.*;
import java.math.BigDecimal;
public class CustomExecutorDemo {
public static void main(String[] args) {
ExecutorService ioPool = Executors.newFixedThreadPool(20);
CompletableFuture<BigDecimal> balance = CompletableFuture
.supplyAsync(() -> new BigDecimal("150000"), ioPool);
CompletableFuture<Void> notify = CompletableFuture
.runAsync(() -> System.out.println("Notification sent"), ioPool);
System.out.println("Balance: Rs." + balance.join());
notify.join();
ioPool.shutdown();
}
}
Notification sent
Balance: Rs.150000
How do you chain transformations after supplyAsync?
CompletableFuture<String> report = CompletableFuture
.supplyAsync(() -> new BigDecimal("250000.50")) // CF<BigDecimal>
.thenApply(bal -> bal.multiply(new BigDecimal("0.065"))) // CF<BigDecimal>
.thenApply(interest -> "Interest: Rs." + interest); // CF<String>
System.out.println(report.join());
Interest: Rs.16250.0325
How do you chain side effects after runAsync?
CompletableFuture.runAsync(() -> System.out.println("Audit log written"))
.thenRun(() -> System.out.println("Dashboard updated"))
.thenRun(() -> System.out.println("All done"))
.join();
Audit log written
Dashboard updated
All done
What happens if you chain thenApply after runAsync?
thenApply receives the value from the previous stage. Since runAsync produces Void, the value passed to thenApply is null, which is rarely useful.
// Not useful: thenApply after runAsync receives null
CompletableFuture.runAsync(() -> sendNotification())
.thenApply(v -> "result"); // v is null (Void)
// If you need a result, use supplyAsync instead
When should you use supplyAsync vs runAsync?
- supplyAsync: balance lookup, interest calculation, credit score fetch, report generation, API call with response.
- runAsync: SMS notification, audit log write, cache warming, metrics reporting, dashboard update.
What are common interview follow-up questions?
- What is the default thread pool for both methods? ForkJoinPool.commonPool().
- Can supplyAsync throw checked exceptions? No, Supplier does not declare throws. Wrap checked exceptions in RuntimeException.
- What happens if the Supplier or Runnable throws? The exception is captured in the CF and surfaces on join() or get().
Visual Overview
supplyAsync runAsync
┌─────────────────┐ ┌─────────────────┐
│ Supplier<T> │ │ Runnable │
│ T get() │ │ void run() │
└────────┬────────┘ └────────┬────────┘
│ │
▼ ▼
CompletableFuture<T> CompletableFuture<Void>
│ │
▼ ▼
join() → T (value) join() → null
thenApply, thenCompose thenRun, thenAccept
Key Takeaway
supplyAsync and runAsync are the two starting points for CompletableFuture pipelines. supplyAsync takes a Supplier that returns a value, producing CF
Both default to ForkJoinPool.commonPool(), which has only cores - 1 threads and is shared across the entire JVM. For IO-bound tasks like database queries and HTTP calls, always pass a dedicated Executor as the second parameter to avoid starving other CompletableFutures and parallel streams.
At National Bank of India, settlement amount calculations use supplyAsync (need the BigDecimal result), while SMS notifications use runAsync (no response needed). Both use a dedicated IO pool with 50 threads instead of the default commonPool.
Remember one sentence: supplyAsync returns a result, runAsync does not, and both should use a custom pool in production.
---