Sitemap
Manish Pandey

Manish Pandey

Software Engineer at Mastercard

Futures In Java

8 min readSep 19, 2026

--

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 Service

Suppose each takes 2 seconds.

If you call them sequentially:

Payment      2 sec
↓
Customer 2 sec
↓
Fraud 2 sec
Total ≈ 6 sec

But if they are independent, you could execute them concurrently:

Payment  ────────── 2 sec
Customer ────────── 2 sec
Fraud ────────── 2 sec
Total ≈ 2 sec

Now 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...
|
↓
Result

Meanwhile, your original thread receives:

Future

You can later ask:

future.get();

for the result.

3. Simple analogy

Imagine ordering food.

You:

Order food
↓
Restaurant

The restaurant gives you:

Order Number = 123

That’s similar to a Future.

The food isn’t ready yet.

Later:

Order #123
↓
Is food ready?
↓
Yes → receive food

Similarly:

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-3

You 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
|
↓
Result

6. 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 = 30

7. 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
↓
30

Meanwhile, 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 seconds

then:

future.get();

can block for up to 5 seconds.

isDone()

future.isDone();

Returns:

true

if 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
|
↓
result

So 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:

TimeoutException

is 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
\ /
\ /
Results

Total execution can be around:

2 seconds

rather than:

4 seconds

assuming enough threads and no other bottlenecks.

12. A real-world example

Imagine your payment application needs:

Customer details
Fraud score
Account details

These 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 ms

Using 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 ms

instead of:

≈ 1500 ms

assuming 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 status

Whereas:

CompletableFuture
|
+-- async execution
+-- chaining
+-- combining
+-- callbacks
+-- exception handling
+-- composition

CompletableFuture is much more powerful.

15. Basic CompletableFuture

CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> {
        return 10 + 20;    });

Later:

Integer result = future.join();

Result:

30

join() is similar to get(), but exception handling differs.

16. thenApply

Suppose you get:

10

and want to transform it:

10 → 20

Use:

CompletableFuture<Integer> future =
CompletableFuture
.supplyAsync(() -> 10)
.thenApply(x -> x * 2);

Conceptually:

supplyAsync
↓
10
↓
thenApply
↓
20

Then:

System.out.println(future.join());

prints:

20

17. 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 CompletableFuture
thenAccept
↓
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 something

This 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 result

21. thenCompose

This is one of the most important advanced concepts.

Imagine:

Get customer
↓
Get customer's account

The second operation depends on the first.

CompletableFuture<Customer> customerFuture =
getCustomerAsync();
CompletableFuture<Account> accountFuture =
customerFuture.thenCompose(
customer -> getAccountAsync(customer.getId())
);

Think:

Customer
↓
need customer ID
↓
Account

This 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
↓
B

Use:

thenCompose()

Example:

Get customer
↓
get customer ID
↓
Get account

This 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:

0

24. 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 value

25. 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 ────────┘
↓
completed

This 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 value
runAsync    → Runnable → no value

29. 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 Service

Suppose 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 result

Potentially 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()
↓
Future

And 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 Order

These 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

--

--

Manish Pandey
Manish Pandey