Futures In Java
Let’s understand Futures in Java from the ground up, then move into Future, Callable, ExecutorService, CompletableFutureexception handling, chaining, parallel execution, and real-world Spring Boot examples.
1. First: Why do we need Future?
Imagine your Java application needs to call three external services:
Your Application
|
+----> Payment Service
|
+----> Customer Service
|
+----> Fraud ServiceSuppose each takes 2 seconds.
If you call them sequentially:
Payment 2 sec
↓
Customer 2 sec
↓
Fraud 2 secTotal ≈ 6 secBut if they are independent, you could execute them concurrently:
Payment ────────── 2 sec
Customer ────────── 2 sec
Fraud ────────── 2 secTotal ≈ 2 secNow we have a problem:
How do we start work in another thread and get the result later?
That’s where Future comes in.
2. What is a Future?
A Future represents the result of an asynchronous computation.
Think of it as a receipt/token for work that is happening somewhere else.
You
|
| submit task
↓
Executor
|
| starts task
↓
Another Thread
|
| computing...
|
↓
ResultMeanwhile, your original thread receives:
FutureYou can later ask:
future.get();for the result.
3. Simple analogy
Imagine ordering food.
You:
Order food
↓
RestaurantThe restaurant gives you:
Order Number = 123That’s similar to a Future.
The food isn’t ready yet.
Later:
Order #123
↓
Is food ready?
↓
Yes → receive foodSimilarly:
Future<Integer> future = executor.submit(task);Then later:
Integer result = future.get();4. ExecutorService
Before using Future, we need to understand ExecutorService.
Instead of manually creating threads:
Thread thread = new Thread(...);
thread.start();Java provides thread pools.
ExecutorService executor =
Executors.newFixedThreadPool(3);This creates a pool containing 3 worker threads.
Conceptually:
ExecutorService
|
+---- Thread-1
|
+---- Thread-2
|
+---- Thread-3You submit tasks to it.
5. Runnable vs Callable
There are two important task types.
Runnable
Runnable doesn’t return a result.
Runnable task = () -> {
System.out.println("Doing work");
};Submit:
executor.submit(task);You don’t get a meaningful result.
Callable
Callable can return a result.
Callable<Integer> task = () -> {
return 10 + 20;
};Now:
Future<Integer> future = executor.submit(task);The relationship is:
Callable
|
| submitted to
↓
ExecutorService
|
↓
Future
|
↓
Result6. First complete example
import java.util.concurrent.*;public class Main { public static void main(String[] args)
throws Exception { ExecutorService executor =
Executors.newFixedThreadPool(2); Callable<Integer> task = () -> { System.out.println(
"Task running on: "
+ Thread.currentThread().getName()
); Thread.sleep(2000); return 10 + 20;
}; Future<Integer> future =
executor.submit(task); System.out.println("Task submitted"); Integer result = future.get(); System.out.println("Result = " + result); executor.shutdown();
}
}
Output will be something like:
Task submitted
Task running on: pool-1-thread-1
Result = 307. What happens step-by-step?
This line:
Future<Integer> future =
executor.submit(task);doesn’t mean:
“Give me the result immediately.”
It means:
“Submit this work and give me a handle to its future result.”
Think:
main thread
|
| submit()
↓
ExecutorService
|
↓
worker thread
|
| executes task
↓
30Meanwhile, the main thread has:
Future<Integer>8. The important Future methods
Future provides several important methods.
get()
future.get();Waits until the task finishes.
If task takes:
5 secondsthen:
future.get();can block for up to 5 seconds.
isDone()
future.isDone();Returns:
trueif the task has completed.
Example:
if (future.isDone()) {
System.out.println("Task finished");
}isCancelled()
future.isCancelled();Checks whether the task was cancelled.
cancel()
future.cancel(true);Attempts to cancel the task.
9. The problem with Future
Here’s an important limitation.
Suppose:
Future<Integer> future =
executor.submit(() -> {Thread.sleep(5000); return 100;
});
Then:
Integer result = future.get();Your current thread waits.
That’s called blocking.
Main Thread
|
| future.get()
|
| WAITING
|
| 5 seconds
|
↓
resultSo although the task is running asynchronously, get() can make your calling thread wait.
10. get(timeout)
You can avoid waiting forever.
Integer result =
future.get(2, TimeUnit.SECONDS);If the task doesn’t finish within 2 seconds:
TimeoutExceptionis thrown.
Example:
try {Integer result =
future.get(2, TimeUnit.SECONDS);} catch (TimeoutException e) { System.out.println("Task took too long");}
This is useful when calling external systems.
11. Multiple Futures
Now let’s make the example more interesting.
Suppose:
Callable<String> paymentTask = () -> {
Thread.sleep(2000);
return "Payment completed";
};Callable<String> customerTask = () -> {
Thread.sleep(2000);
return "Customer fetched";
};Submit both:
Future<String> paymentFuture =
executor.submit(paymentTask);Future<String> customerFuture =
executor.submit(customerTask);Both can execute concurrently.
Then:
String payment = paymentFuture.get();
String customer = customerFuture.get();Conceptually:
Executor
/ \
/ \
Payment Customer
2 sec 2 sec
\ /
\ /
ResultsTotal execution can be around:
2 secondsrather than:
4 secondsassuming enough threads and no other bottlenecks.
12. A real-world example
Imagine your payment application needs:
Customer details
Fraud score
Account detailsThese calls don’t depend on each other.
Sequential:
Customer customer = getCustomer();FraudScore fraud = getFraudScore();Account account = getAccount();
If each takes 500 ms:
500 + 500 + 500
= 1500 msUsing Future:
Future<Customer> customerFuture =
executor.submit(() -> getCustomer());Future<FraudScore> fraudFuture =
executor.submit(() -> getFraudScore());Future<Account> accountFuture =
executor.submit(() -> getAccount());
Then:
Customer customer = customerFuture.get();FraudScore fraud = fraudFuture.get();Account account = accountFuture.get();
Potentially:
≈ 500 msinstead of:
≈ 1500 msassuming all three operations can genuinely run concurrently.
13. But there’s a better modern API
Java 8 introduced:
CompletableFuture
This is extremely important for modern Java development.
Get Manish Pandey’s stories in your inbox
Join Medium for free to get updates from this writer.
CompletableFuture builds upon the Future concept and provides:
- asynchronous execution
- chaining
- combining tasks
- exception handling
- callbacks
- composition
Instead of:
Future<Integer> future =
executor.submit(task);Integer result = future.get();you can use:
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> {
return 10 + 20;
});Then:
future.thenAccept(result -> {
System.out.println(result);
});14. Future vs CompletableFuture
Think:
Future
|
+-- submit task
+-- get result
+-- cancel
+-- check statusWhereas:
CompletableFuture
|
+-- async execution
+-- chaining
+-- combining
+-- callbacks
+-- exception handling
+-- compositionCompletableFuture is much more powerful.
15. Basic CompletableFuture
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> {return 10 + 20; });
Later:
Integer result = future.join();Result:
30join() is similar to get(), but exception handling differs.
16. thenApply
Suppose you get:
10and want to transform it:
10 → 20Use:
CompletableFuture<Integer> future =
CompletableFuture
.supplyAsync(() -> 10)
.thenApply(x -> x * 2);Conceptually:
supplyAsync
↓
10
↓
thenApply
↓
20Then:
System.out.println(future.join());prints:
2017. thenApply is like a pipeline
This is a powerful mental model.
CompletableFuture<String> result =
CompletableFuture
.supplyAsync(() -> "manish")
.thenApply(String::toUpperCase)
.thenApply(name -> "Hello " + name);Pipeline:
"manish"
↓
"MANISH"
↓
"Hello MANISH"18. thenAccept
If you don’t need to return another value:
CompletableFuture
.supplyAsync(() -> "Hello")
.thenAccept(result -> {
System.out.println(result);
});Difference:
thenApply
↓
transforms value
↓
returns CompletableFuturethenAccept
↓
consumes value
↓
returns CompletableFuture<Void>19. thenRun
If you don’t care about the previous result:
CompletableFuture
.supplyAsync(() -> "Hello")
.thenRun(() -> {
System.out.println("Finished!");
});Think:
thenApply → transform
thenAccept → consume
thenRun → just execute somethingThis distinction is very useful.
20. Combining independent tasks
Suppose:
CompletableFuture<String> customer =
CompletableFuture.supplyAsync(() ->
getCustomer()
);CompletableFuture<String> account =
CompletableFuture.supplyAsync(() ->
getAccount()
);You want both results.
Use:
CompletableFuture<String> combined =
customer.thenCombine(
account,
(customerResult, accountResult) ->
customerResult + " " + accountResult
);Conceptually:
customer ───────┐
↓
combine
↓
account ───────┘
↓
final result21. thenCompose
This is one of the most important advanced concepts.
Imagine:
Get customer
↓
Get customer's accountThe second operation depends on the first.
CompletableFuture<Customer> customerFuture =
getCustomerAsync();CompletableFuture<Account> accountFuture =
customerFuture.thenCompose(
customer -> getAccountAsync(customer.getId())
);Think:
Customer
↓
need customer ID
↓
AccountThis is a dependency.
22. thenCombine vs thenCompose
Very important.
Independent operations
A ──────┐
↓
combine
↑
B ──────┘Use:
thenCombine()Example:
Get customer ──────┐
↓
combine
↑
Get account ───────┘Dependent operations
A
↓
BUse:
thenCompose()Example:
Get customer
↓
get customer ID
↓
Get accountThis distinction is extremely important.
23. Exception handling
What happens if an asynchronous task fails?
You can use:
exceptionally()Example:
CompletableFuture<Integer> future =
CompletableFuture
.supplyAsync(() -> {
throw new RuntimeException("Something failed");
})
.exceptionally(ex -> {
System.out.println(
"Error: " + ex.getMessage()
); return 0;
});Now the pipeline can recover with:
024. handle()
handle() receives both the result and exception.
CompletableFuture<Integer> future =
CompletableFuture
.supplyAsync(() -> 10 / 0)
.handle((result, exception) -> {if (exception != null) {
return 0;
} return result;
});
Think:
task
/ \
success failure
\ /
handle
↓
final value25. whenComplete()
If you want to observe success/failure but not transform it:
future.whenComplete((result, exception) -> {if (exception != null) {
System.out.println("Failed");
} else {
System.out.println("Result = " + result);
}});
Useful for logging, metrics, etc.
26. allOf
Suppose you have:
CompletableFuture<String> f1 = ...;
CompletableFuture<String> f2 = ...;
CompletableFuture<String> f3 = ...;You want to wait until all three finish.
Use:
CompletableFuture<Void> all =
CompletableFuture.allOf(f1, f2, f3);Then:
all.join();Conceptually:
f1 ────────┐
│
f2 ────────┼──→ allOf
│
f3 ────────┘
↓
completedThis is extremely useful when you have multiple parallel operations.
27. anyOf
Sometimes you only care about the first completed task.
CompletableFuture<Object> first =
CompletableFuture.anyOf(f1, f2, f3);Conceptually:
f1 ─────── 3 sec
f2 ─────── 1 sec ← WIN
f3 ─────── 5 sec↓ anyOf
↓
f2 result
Useful for things like multiple fallback providers, though production use requires careful cancellation/error handling.
28. supplyAsync vs runAsync
Another important distinction.
supplyAsync
Returns a value.
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> {
return 100;
});runAsync
Doesn’t return a value.
CompletableFuture<Void> future =
CompletableFuture.runAsync(() -> {
System.out.println("Running");
});Think:
supplyAsync → Supplier → returns valuerunAsync → Runnable → no value29. Custom Executor
By default:
CompletableFuture.supplyAsync(...)uses a common ForkJoinPool.
But in production applications, you may want your own executor.
ExecutorService executor =
Executors.newFixedThreadPool(10);Then:
CompletableFuture<String> future =
CompletableFuture.supplyAsync(
() -> callPaymentService(),
executor
);This gives you control over:
- thread count
- naming
- queueing
- isolation
- shutdown
- workload separation
This is especially important in Spring Boot applications.
30. Real Spring Boot example
Imagine:
Transaction Service
|
+---- Customer Service
|
+---- Fraud Service
|
+---- Account ServiceSuppose these calls are independent.
You could do:
CompletableFuture<Customer> customer =
CompletableFuture.supplyAsync(
() -> customerService.getCustomer(id),
executor
);CompletableFuture<FraudResult> fraud =
CompletableFuture.supplyAsync(
() -> fraudService.check(id),
executor
);CompletableFuture<Account> account =
CompletableFuture.supplyAsync(
() -> accountService.getAccount(id),
executor
);
Then:
CompletableFuture<Void> all =
CompletableFuture.allOf(
customer,
fraud,
account
);Once all complete:
all.join();Customer c = customer.join();
FraudResult f = fraud.join();
Account a = account.join();Conceptually:
Transaction
|
+------------+------------+
| | |
↓ ↓ ↓
Customer Fraud Account
500ms 400ms 600ms
| | |
+------------+------------+
|
↓
Combined resultPotentially the latency approaches the slowest independent call rather than the sum, subject to network, thread-pool, service, and other constraints.
31. Future vs CompletableFuture — interview answer
If an interviewer asks:
What is Future?
You can say:
Future represents the result of an asynchronous computation. It allows us to submit a task through an ExecutorService and retrieve the result later using methods such as get(). It also provides methods to check completion and cancel tasks. However, Future.get() is blocking and Future has limited support for composing asynchronous operations.
Then:
CompletableFuture, introduced in Java 8, extends the Future model and provides a fluent API for composing, chaining, combining and handling asynchronous computations without manually blocking between every step.
That’s a strong interview answer.
32. The hierarchy you should understand
Don’t think of these as unrelated APIs.
Think:
Thread
↓
ExecutorService
↓
submit()
↓
FutureAnd modern Java:
CompletableFuture
|
+── supplyAsync()
|
+── thenApply()
|
+── thenAccept()
|
+── thenRun()
|
+── thenCombine()
|
+── thenCompose()
|
+── exceptionally()
|
+── handle()
|
+── allOf()
|
+── anyOf()33. The most important mental model
Learn these four patterns:
Pattern 1 — Start async work
CompletableFuture.supplyAsync(() -> doSomething());Pattern 2 — Transform result
.thenApply(result -> transform(result))Pattern 3 — Independent operations
futureA.thenCombine(
futureB,
(a, b) -> combine(a, b)
);Pattern 4 — Dependent operations
futureA.thenCompose(
a -> operationUsingA(a)
);If you understand these four, you’ve understood the core of CompletableFuture.
One final example tying everything together
Imagine an e-commerce request:
Get Product
↓
Get Price
↓
Apply Discount
↓
Create OrderThese are dependent:
CompletableFuture
.supplyAsync(() -> getProduct())
.thenCompose(product ->
getPriceAsync(product)
)
.thenApply(price ->
applyDiscount(price)
)
.thenCompose(finalPrice ->
createOrderAsync(finalPrice)
)
.exceptionally(ex -> {
System.out.println("Order failed");
return null;
});The mental model is:
supplyAsync
↓
Product
↓
thenCompose
↓
Price
↓
thenApply
↓
Discounted Price
↓
thenCompose
↓
Order
↓
exceptionally





