Back to CompletableFuture & Resilience
Q2Question 2 of 19

supplyAsync() vs runAsync()?

Key Concepts
  • supplyAsync(Supplier<T>) starts an async task that returns a result, giving CF<T>.
  • runAsync(Runnable) starts an async task with no return value, giving CF<Void> where get() returns null.
  • Both default to ForkJoinPool.commonPool(); pass a custom Executor as the second parameter for IO-bound work.
  • Use supplyAsync for tasks where you need the result: balance lookup, calculation, API call.
  • Use runAsync for fire-and-forget tasks: logging, notifications, cache warming.
  • Chain thenApply after supplyAsync for transformations; chain thenRun after runAsync for sequential side effects.

Definition

supplyAsync(Supplier<T>) starts an async task that returns a result as CF<T>, while runAsync(Runnable) starts an async task with no return value as CF<Void>; both default to ForkJoinPool.commonPool().
  • supplyAsync(Supplier) – starts an async task using a Supplier whose get() method returns T; the result is stored in CompletableFuture.
  • runAsync(Runnable) – starts an async task using a Runnable with no return value; returns CompletableFuture where get() returns null.
  • Default pool – both use ForkJoinPool.commonPool() unless a custom Executor is 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.

JAVA
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);
    }
}
TEXT
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.

JAVA
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");
    }
}
TEXT
SMS sent to Rajesh: Trade TRD-001 settled
Notification complete

How do supplyAsync and runAsync compare?

| Feature | supplyAsync | runAsync |
|---|---|---|
| Input | Supplier (T get()) | Runnable (void run()) |
| Return | CF | 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.

JAVA
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();
    }
}
TEXT
Notification sent
Balance: Rs.150000

How do you chain transformations after supplyAsync?

JAVA
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());
TEXT
Interest: Rs.16250.0325

How do you chain side effects after runAsync?

JAVA
CompletableFuture.runAsync(() -> System.out.println("Audit log written"))
    .thenRun(() -> System.out.println("Dashboard updated"))
    .thenRun(() -> System.out.println("All done"))
    .join();
TEXT
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.

JAVA
// 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

DIAGRAM
  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. Use it when you need the result: balance lookup, interest calculation, API call. runAsync takes a Runnable with no return value, producing CF. Use it for fire-and-forget work: notifications, logging, cache warming.

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.

---

Share
Use ← → arrow keys to navigate