✍️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 and Future 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 to 20.
  • thenApply(value -> value + 5) transforms it to 25.

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 returns 10, and future2 returns 20.
  • 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 returns 0.

📌 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 and 3s).
  • In real-world, replace Thread.sleep() with an actual API call (e.g., HttpClient or RestTemplate).

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 from User API
    2️⃣ User orders from Order 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 using supplyAsync().
  • 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 :)

--

--

Responses (1)