✍️CompletableFuture in Java 8-A new era of asynchronous programming✍️
📌 What is Asynchronous Programming?
Normally, when you run code, it executes line by line. If a task takes a long time (e.g., calling an API), the whole program waits (blocking).
✅ Asynchronous programming means:
- The program doesn’t wait for a task to finish.
- The task runs on a separate thread in the background.
- The program can do other work while waiting.
💚What is CompletableFuture?💚
CompletableFuture
is a class in Java used for asynchronous programming. Asynchronous programming means writing non-blocking code, allowing tasks to execute on a separate thread without blocking the main application thread.
With CompletableFuture
, the main thread does not wait for task completion. Instead, other tasks can execute in parallel, improving the performance of the program.
👉Key Features of CompletableFuture
- Runs tasks asynchronously on a separate thread.
- Notifies the main thread upon progress, completion, or failure.
- Improves parallel execution and performance.
- Provides methods for chaining tasks and handling exceptions efficiently.
👉Technical Details
CompletableFuture
is a class in Java 8.- It belongs to the
java.util.concurrent
package. - It implements both the
CompletionStage
andFuture
interfaces.
🔔Why CompletableFuture?
Before CompletableFuture, Java provided several mechanisms for asynchronous programming, such as:
Future<T>
(from Java 5)ExecutorService
- Thread Pools
- Callback Interfaces
However, these approaches had several limitations that made them less flexible. CompletableFuture was introduced in Java 8 to overcome these drawbacks.
🚨Limitations of Traditional Future Approach
1. Future cannot be manually completed
- Once you submit a
Future
task, it automatically runs in the background. - You cannot manually complete or change its result.
🔹 Example:
Future<Integer> future = executorService.submit(() -> 10);
future.complete(20); // ❌ This is not possible in Future!
🔹 How CompletableFuture solves it?
CompletableFuture<Integer> completableFuture = new CompletableFuture<>();
completableFuture.complete(20); // ✅ This works!
👉 With CompletableFuture, you can manually complete tasks.
2. Futures cannot be chained together
- In
Future<T>
, you cannot execute another task once the first one is done. - You have to manually check if the
Future
is completed and then trigger another operation.
🔹 Example (Future Limitation):
Future<Integer> future = executorService.submit(() -> 10);
Integer result = future.get(); // ❌ This blocks the main thread
Integer finalResult = result * 2; // Manual chaining required
🔹 How CompletableFuture solves it?
CompletableFuture.supplyAsync(() -> 10)
.thenApply(result -> result * 2)
.thenAccept(System.out::println); // ✅ Output: 20
👉 With CompletableFuture, you can chain multiple operations seamlessly.
3. Cannot combine multiple Futures
- With
Future
, if you need to run multiple tasks in parallel, you have to manually manage them. - There is no direct way to combine multiple
Future
results.
🔹 Example (Future Limitation):
Future<Integer> future1 = executorService.submit(() -> 10);
Future<Integer> future2 = executorService.submit(() -> 20);
Integer sum = future1.get() + future2.get(); // ❌ Manual merging & blocking
🔹 How CompletableFuture solves it?
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
future1.thenCombine(future2, Integer::sum)
.thenAccept(System.out::println); // ✅ Output: 30
👉 With CompletableFuture, you can combine multiple tasks efficiently.
4. No Proper Exception Handling in Future
- In
Future
, if an exception occurs inside a task, you must handle it using try-catch. - Otherwise, it can crash your application.
🔹 Example (Future Limitation)
Future<Integer> future = executorService.submit(() -> {
throw new RuntimeException("Error occurred!");
});
future.get(); // ❌ Throws ExecutionException
🔹 How CompletableFuture solves it?
CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Error occurred!");
return 10;
}).exceptionally(ex -> {
System.out.println("Handled Exception: " + ex.getMessage());
return 0;
}).thenAccept(System.out::println);
// ✅ Output: Handled Exception: Error occurred!
👉 With CompletableFuture, you can handle exceptions properly using exceptionally()
.
📌 Key Methods in CompletableFuture
👉 Think of CompletableFuture
as ordering food in a restaurant:
1️⃣ You place an order (CompletableFuture
).
2️⃣ While the chef is cooking (background task), you do other things.
3️⃣ When the food is ready, the waiter notifies you (completion).
1. Simple Example: Running a Task Asynchronously
import java.util.concurrent.CompletableFuture;
public class AsyncExample {
public static void main(String[] args) {
System.out.println("Task started...");
CompletableFuture<Void> future =
CompletableFuture.runAsync(() -> {
try { Thread.sleep(2000); }
catch (InterruptedException e) {}
System.out.println("Task completed!");
});
System.out.println("Main thread is free to do other things...");
future.join(); // Wait for task to complete
}
}
🔹 Output
Task started...
Main thread is free to do other things...
Task completed!
✅ The main thread does not wait!
✅ The task runs in the background and completes after 2 seconds.
2. Fetch Data Asynchronously Using supplyAsync()
import java.util.concurrent.CompletableFuture;
public class FetchDataExample {
public static void main(String[] args) {
System.out.println("Fetching data...");
CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(3000); }
catch (InterruptedException e) {}
return "Data from server";
});
System.out.println("Main thread can do other tasks...");
String result = future.join(); // Waits for result
System.out.println("Received: " + result);
}
}
🔹 Output
Fetching data...
Main thread can do other tasks...
Received: Data from server
✅ Does not block the main thread while waiting for data.
✅ Returns the result when ready.
3. Chaining Multiple Tasks Using thenApply()
We can transform the result of a CompletableFuture
using thenApply()
.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> 10)
.thenApply(value -> value * 2) // Multiply by 2
.thenApply(value -> value + 5); // Add 5
System.out.println("Result: " + future.join());
}
}
Output
Result: 25
👉 Explanation:
- The first task returns
10
. thenApply(value -> value * 2)
transforms it to20
.thenApply(value -> value + 5)
transforms it to25
.
4. Running Parallel Tasks Using thenCombine()
We can combine results from two CompletableFutures
and process them together.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Integer> future1 = CompletableFuture.supplyAsync(() -> 10);
CompletableFuture<Integer> future2 = CompletableFuture.supplyAsync(() -> 20);
CompletableFuture<Integer> result = future1.thenCombine(future2, (num1, num2) -> num1 + num2);
System.out.println("Sum: " + result.join());
}
}
Output
Sum: 30
👉 Explanation:
future1
returns10
, andfuture2
returns20
.thenCombine()
adds them (10 + 20 = 30
)
5. Handling Exceptions with exceptionally()
If an exception occurs, we can handle it gracefully using exceptionally()
.
import java.util.concurrent.CompletableFuture;
public class CompletableFutureExample {
public static void main(String[] args) {
CompletableFuture<Integer> future =
CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Something went wrong!");
return 10;
}).exceptionally(ex -> {
System.out.println("Exception: " + ex.getMessage());
return 0; // Default value
});
System.out.println("Result: " + future.join());
}
}
Output
Exception: java.lang.RuntimeException: Something went wrong!
Result: 0
👉 Explanation:
- The task throws an exception.
exceptionally()
catches the exception and returns0
.
📌 Why Use CompletableFuture
?
✅ Runs tasks in parallel (Faster execution).
✅ Prevents blocking the main thread.
✅ Easily handle multiple tasks at once (e.g., API calls).
✅ Built-in error handling.
🔴Fetch Data from Two APIs in Parallel Using CompletableFuture.supplyAsync()?
Problem Statement
- Fetch data from two APIs concurrently using
CompletableFuture.supplyAsync()
. - Combine the results once both API calls are completed.
- Return the combined response.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class ParallelApiFetcher {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// Fetch and combine API data
String result = fetchAndCombineData();
System.out.println("Final Combined API Response: " + result);
}
public static String fetchAndCombineData() throws ExecutionException, InterruptedException {
// Call API 1 asynchronously
CompletableFuture<String> api1Future =
CompletableFuture.supplyAsync(() -> fetchFromApi1());
// Call API 2 asynchronously
CompletableFuture<String> api2Future =
CompletableFuture.supplyAsync(() -> fetchFromApi2());
// Wait for both to complete and combine results
return api1Future.thenCombine(api2Future, (api1Data, api2Data) ->
"API1: " + api1Data + " | API2: " + api2Data)
.get(); // Get final result
}
// Simulated API call 1
private static String fetchFromApi1() {
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Data from API 1";
}
// Simulated API call 2
private static String fetchFromApi2() {
try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Data from API 2";
}
}
🔹 Step-by-Step Explanation
🟢 Step 1: Call API 1 & API 2 in Parallel
CompletableFuture<String> api1Future =
CompletableFuture.supplyAsync(() -> fetchFromApi1());
CompletableFuture<String> api2Future =
CompletableFuture.supplyAsync(() -> fetchFromApi2());
CompletableFuture.supplyAsync()
runs both API calls simultaneously.- API 1 and API 2 start at the same time instead of waiting for one to finish.
- No blocking! The main program keeps running.
🟢 Step 2: Combine Results When Both APIs Are Done
return api1Future.thenCombine(api2Future, (api1Data, api2Data) ->
"API1: " + api1Data + " | API2: " + api2Data)
.get(); // Blocks only at the end
thenCombine()
waits for both API calls to finish.- It combines the results using
(api1Data, api2Data) -> "API1: " + api1Data + " | API2: " + api2Data"
. .get()
waits for both API calls to complete and returns the final response.
🟢 Step 3: Simulated API Calls (fetchFromApi1()
& fetchFromApi2()
)
private static String fetchFromApi1() {
try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Data from API 1";
}
private static String fetchFromApi2() {
try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
return "Data from API 2";
}
- Simulates two APIs taking different times (
2s
and3s
). - In real-world, replace
Thread.sleep()
with an actual API call (e.g.,HttpClient
orRestTemplate
).
Processing Steps:
API 1 starts (takes 2s)
API 2 starts (takes 3s)
Both APIs run simultaneously
Final Output (After 3s Total Execution Time):
Final Combined API Response: API1: Data from API 1 | API2: Data from API 2
⏳ Time Taken: 3 seconds (Instead of 5 seconds if run sequentially).
🚀 Using CompletableFuture.supplyAsync()
in a Real-Time Spring Boot Project
📌 Real-World Use Case
In a Spring Boot application, we often need to fetch data from multiple microservices or external APIs in parallel to improve performance.
✅ Example Scenario:
- A User Service needs to fetch:
1️⃣ User details fromUser API
2️⃣ User orders fromOrder API
- Both APIs should be called in parallel to speed up response time.
✅ Real-Time Spring Boot Implementation
1️⃣ Create a Spring Boot Service with CompletableFuture
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.concurrent.CompletableFuture;
@Service
public class UserService {
private final RestTemplate restTemplate = new RestTemplate();
public CompletableFuture<String> fetchUserDetails(String userId) {
return CompletableFuture.supplyAsync(() -> {
String url = "https://api.example.com/users/" + userId;
return restTemplate.getForObject(url, String.class); // Call User API
});
}
public CompletableFuture<String> fetchUserOrders(String userId) {
return CompletableFuture.supplyAsync(() -> {
String url = "https://api.example.com/orders/" + userId;
return restTemplate.getForObject(url, String.class); // Call Order API
});
}
public CompletableFuture<String> fetchCombinedUserData(String userId) {
CompletableFuture<String> userFuture = fetchUserDetails(userId);
CompletableFuture<String> orderFuture = fetchUserOrders(userId);
return userFuture.thenCombine(orderFuture, (userData, orderData) ->
"User Details: " + userData + " | Orders: " + orderData
);
}
}
2️⃣ Create a Spring Boot Controller
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@RestController
@RequestMapping("/api")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/user/{userId}")
public String getUserInfo(@PathVariable String userId) throws ExecutionException, InterruptedException {
return userService.fetchCombinedUserData(userId).get(); // Blocking call (can use async in real cases)
}
}
🔹 Step-by-Step Explanation
Step 1: Fetch User Details and Orders in Parallel
public CompletableFuture<String> fetchUserDetails(String userId) {
return CompletableFuture.supplyAsync(() -> {
String url = "https://api.example.com/users/" + userId;
return restTemplate.getForObject(url, String.class);
});
}
- Calls
User API
asynchronously usingsupplyAsync()
. RestTemplate.getForObject(url, String.class)
fetches data from the API.
Step 2: Fetch User Orders in Parallel
public CompletableFuture<String> fetchUserOrders(String userId) {
return CompletableFuture.supplyAsync(() -> {
String url = "https://api.example.com/orders/" + userId;
return restTemplate.getForObject(url, String.class);
});
}
- Calls
Order API
simultaneously with the User API. - Both methods run independently, reducing total execution time.
Step 3: Combine the Results
public CompletableFuture<String> fetchCombinedUserData(String userId) {
CompletableFuture<String> userFuture = fetchUserDetails(userId);
CompletableFuture<String> orderFuture = fetchUserOrders(userId);
return userFuture.thenCombine(orderFuture, (userData, orderData) ->
"User Details: " + userData + " | Orders: " + orderData
);
}
thenCombine()
waits for both futures to complete and merges results.- Final Response Example:
User Details: { "id": 1, "name": "John Doe" } | Orders: { "orderId": 123, "total": 250 }
Step 4: Expose API in Controller
@GetMapping("/user/{userId}")
public String getUserInfo(@PathVariable String userId) throws ExecutionException, InterruptedException {
return userService.fetchCombinedUserData(userId).get(); // Blocking to get result
}
- The controller exposes
/api/user/{userId}
to call both APIs in parallel. .get()
waits for the final result before returning the response.
🔹 User Calls API
GET http://localhost:8080/api/user/1
1️⃣ fetchUserDetails("1")
calls User API
.
2️⃣ fetchUserOrders("1")
calls Order API
.
3️⃣ Both APIs run simultaneously in parallel.
4️⃣ thenCombine()
merges both results.
5️⃣ The final response is returned faster (because of parallel execution).
Happy Learning :)