Java Async Programming - CompletableFuture

Background

CompletableFuture was introduced as a Java 8 and is helpful for asynchronous programming. It allows developers to write non-blocking code and execute tasks in parallel. However, it can be challenging to understand how to use CompletableFuture effectively. This post will provide a comprehensive guide to using CompletableFuture in Java.

Create CompletableFuture

  1. The supplyAsync method is used to create a CompletableFuture that returns a value, while the runAsync method is used to create a CompletableFuture that does not return a value.
1
2
3
4
5
6
7
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier){..}  

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor){..}  

public static CompletableFuture<Void> runAsync(Runnable runnable){..}  

public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor){..}  

Get Result

  1. The get method is used to retrieve the result of a CompletableFuture. It is a blocking call that waits for the result to be available.

  2. the get with timeout method is used to retrieve the result of a CompletableFuture with a timeout. It is a blocking call that waits for the result to be available within the specified time.

  3. the getNow method is used to retrieve the result of a CompletableFuture. It returns the specified value if the result is not available.

  4. the join method is used to retrieve the result of a CompletableFuture. It is a non-blocking call that waits for the result to be available. No checked exception is thrown.

1
2
3
4
5
6
7
public T get()  

public T get(long timeout, TimeUnit unit)  

public T getNow(T valueIfAbsent)  

public T join()

Callback

thenRun/thenRunAsync

  1. the 2nd task doest not dependent on the 1st stage’s result

  2. the 2nd task does not return a value

1
2
3
4
5
public CompletableFuture<Void> thenRun(Runnable action)

public CompletableFuture<Void> thenRunAsync(Runnable action)

public CompletableFuture<Void> thenRunAsync(Runnable action Executor executor)

The difference between thenRun and thenRunAsync is that thenRun runs in the same thread as the previous stage, while thenRunAsync runs in a different thread. For example, the 1st task is executed in the defined thread pool, and the 2nd task is executed in the forkJoin pool if no other pool is specified. Same difference between other methods.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String[] args) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(2);

    System.out.println("\n=== thenRun vs thenRunAsync ===");

    CompletableFuture.supplyAsync(() -> {
        System.out.println("First task running in thread: " + Thread.currentThread().getName());
        return "Some Result";
    }, executor).thenRun(() -> {
        System.out.println("thenRun - Second task running in thread: " + Thread.currentThread().getName());
    });

    CompletableFuture.supplyAsync(() -> {
        System.out.println("First task running in thread: " + Thread.currentThread().getName());
        return "Some Result";
    }, executor).thenRunAsync(() -> {
        System.out.println("thenRunAsync - Second task running in thread: " + Thread.currentThread().getName());
    });

    executor.shutdown();
    executor.awaitTermination(5, TimeUnit.SECONDS);
}
1
2
3
4
5
=== thenRun vs thenRunAsync ===
First task running in thread: pool-1-thread-1
thenRun - Second task running in thread: main
First task running in thread: pool-1-thread-2
thenRunAsync - Second task running in thread: ForkJoinPool.commonPool-worker-1

thenAccept/thenAcceptAsync

  1. the 2nd task can access the 1st stage’s result

  2. the 2nd task does not return a value

1
2
3
4
5
public CompletableFuture<Void> thenAccept(Consumer<? super T> action)

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)

public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)
1
2
3
4
5
6
7
8
System.out.println("\n=== thenAccept Example ===");
CompletableFuture.supplyAsync(() -> {
    System.out.println("Calculating price...");
    return 100.0; // Simulating price calculation
}, executor).thenAccept(price -> {
    System.out.println("Price received: $" + price);
    // Process the price but don't return anything
});
1
2
3
=== thenAccept Example ===
Calculating price...
Price received: $100.0

thenApply/thenApplyAsync

  1. the 2nd task can access the 1st stage’s result

  2. the 2nd task returns a value

1
2
3
4
5
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)

public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
1
2
3
4
5
6
7
8
9
System.out.println("\n=== thenApply Example ===");
CompletableFuture<Double> finalPrice = CompletableFuture.supplyAsync(() -> {
    System.out.println("Calculating base price...");
    return 100.0; // Base price
}, executor).thenApply(price -> {
    System.out.println("Adding tax...");
    return price * 1.2; // Adding 20% tax
});
System.out.println("Final price with tax: $" + finalPrice.get());
1
2
3
4
=== thenApply Example ===
Calculating base price...
Adding tax...
Final price with tax: $120.0

thenCompose/thenComposeAsync

  1. The thenCompose method is used to chain multiple CompletableFutures together.

  2. The thenCompose method takes a Function that returns a CompletableFuture.

  3. The differenvce between thenApply and thenCompose is that thenApply returns a value, while thenCompose returns a CompletableFuture. If you retuen a CompletableFuture in thenApply, it will be wrapped in another CompletableFuture, which means you will have a nested CompletableFuture. So use thenCompose if you want to chain multiple CompletableFutures together.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
System.out.println("\n=== thenApply vs thenCompose Difference ===");

// Using thenApply - results in nested CompletableFuture
CompletableFuture<CompletableFuture<String>> nestedFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("First task in thenApply: " + Thread.currentThread().getName());
    return "user123";
}, executor).thenApply(userId -> {
    // This returns a CompletableFuture, but it gets wrapped in another CompletableFuture
    return CompletableFuture.supplyAsync(() -> {
        System.out.println("Second task in thenApply: " + Thread.currentThread().getName());
        return "User details for: " + userId;
    }, executor);
});

// Using thenCompose - flattens the nested CompletableFuture
CompletableFuture<String> flatFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("First task in thenCompose: " + Thread.currentThread().getName());
    return "user123";
}, executor).thenCompose(userId -> {
    // This returns a CompletableFuture, and thenCompose flattens it
    return CompletableFuture.supplyAsync(() -> {
        System.out.println("Second task in thenCompose: " + Thread.currentThread().getName());
        return "User details for: " + userId;
    }, executor);
});

System.out.println("thenApply result type: " + nestedFuture.get().get()); // Note the double get()
System.out.println("thenCompose result type: " + flatFuture.get()); // Single get()
1
2
3
4
5
6
7
=== thenApply vs thenCompose Difference ===
First task in thenApply: pool-1-thread-1
Second task in thenApply: pool-1-thread-2
First task in thenCompose: pool-1-thread-3
thenApply result type: User details for: user123
Second task in thenCompose: pool-1-thread-4
thenCompose result type: User details for: user123

Exception Handling

whenComplete

  1. The whenComplete method is called when the CompletableFuture completes, regardless of whether it completed successfully or exceptionally.

  2. If the CompletableFuture completes successfully, the whenComplete method receives the result and the exception is null.

  3. If the CompletableFuture completes exceptionally, the whenComplete method receives null as the result and the exception.

  4. The exception will be thrown if the get method is called on the CompletableFuture and the exception is not swallowed.

1
2
3
4
5
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)

public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)

public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Example 1: Successful completion
System.out.println("\n=== Successful Completion Example ===");
CompletableFuture<Integer> successFuture = CompletableFuture
        .supplyAsync(() -> {
            System.out.println("Calculating result in thread: " + Thread.currentThread().getName());
            return 42;
        }, executor)
        .whenComplete((result, ex) -> {
            if (ex == null) {
                System.out.println("Operation completed successfully with result: " + result);
            } else {
                System.out.println("Operation failed with exception: " + ex.getMessage());
            }
            System.out.println("whenComplete running in thread: " + Thread.currentThread().getName());
        });

System.out.println("Success result: " + successFuture.get());

// Example 2: Exceptional completion
System.out.println("\n=== Exceptional Completion Example ===");
CompletableFuture<Integer> errorFuture = CompletableFuture
        .<Integer>supplyAsync(() -> {
            System.out.println("Throwing exception in thread: " + Thread.currentThread().getName());
            throw new IllegalStateException("Simulated error");
        }, executor)
        .whenComplete((result, ex) -> {
            if (ex == null) {
                System.out.println("Operation completed successfully with result: " + result);
            } else {
                System.out.println("Operation failed with exception: " + ex.getMessage());
            }
            System.out.println("whenComplete running in thread: " + Thread.currentThread().getName());
        });

try {
    errorFuture.get(); // This will throw an exception
} catch (Exception e) {
    System.out.println("Caught exception from get(): " + e.getCause().getMessage());
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
=== Successful Completion Example ===
Calculating result in thread: pool-1-thread-1
Operation completed successfully with result: 42
whenComplete running in thread: pool-1-thread-1
Success result: 42

=== Exceptional Completion Example ===
Throwing exception in thread: pool-1-thread-2
Operation failed with exception: java.lang.IllegalStateException: Simulated error
whenComplete running in thread: pool-1-thread-2
Caught exception from get(): Simulated error

exceptionally

  1. the exceptionally method is called when the CompletableFuture completes exceptionally.
1
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
System.out.println("\n=== Basic Exception Handling ===");
CompletableFuture<Integer> divisionFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("Attempting division...");
    return 100 / 0; // Will throw ArithmeticException
}, executor).exceptionally(throwable -> {
    System.out.println("Exception caught: " + throwable.getMessage());
    return -1; // Return default value in case of error
});

System.out.println("Division result: " + divisionFuture.get());
1
2
3
4
=== Basic Exception Handling ===
Attempting division...
Exception caught: java.lang.ArithmeticException: / by zero
Division result: -1

Common Issues & Notes

  1. Slient Failures
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// WRONG - Exception gets swallowed
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).thenAccept(result -> {
    // This never executes due to exception
    System.out.println(result);
});

// CORRECT - Handle the exception
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).whenComplete((result, ex) -> {
    if (ex != null) {
        System.err.println("Error occurred: " + ex.getMessage());
    }
}).exceptionally(throwable -> {
    // Provide fallback value
    return null;
});
  1. Chain Breaking
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// WRONG - Chain breaks after exception
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).thenApply(result -> {
    return "Processed: " + result; // Never executes
}).thenAccept(System.out::println);

// CORRECT - Handle exception and continue chain
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Error");
}).exceptionally(ex -> {
    return "Default Value"; // Provide fallback
}).thenApply(result -> {
    return "Processed: " + result; // Continues with fallback value
}).thenAccept(System.out::println);
  1. Exception Propagation in Composed Futures
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// WRONG - Lost exception in nested future
CompletableFuture.supplyAsync(() -> {
    return CompletableFuture.supplyAsync(() -> {
        throw new RuntimeException("Nested Error");
    });
}).thenAccept(System.out::println);

// CORRECT - Use thenCompose to properly propagate exceptions
CompletableFuture.supplyAsync(() -> "value")
    .thenCompose(value -> CompletableFuture.supplyAsync(() -> {
        throw new RuntimeException("Nested Error");
    }))
    .exceptionally(ex -> {
        System.err.println("Caught: " + ex.getMessage());
        return "fallback";
    });
  1. Lost Context in Async Methods
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// WRONG - Exception context might be lost
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Original error");
}).thenApplyAsync(result -> {
    // Exception context might be different thread
    return result;
});

// CORRECT - Preserve context with proper exception handling
CompletableFuture.supplyAsync(() -> {
    throw new RuntimeException("Original error");
}).whenComplete((result, ex) -> {
    // Capture exception context here
    if (ex != null) {
        // Log with full context
        logger.error("Operation failed", ex);
    }
});
  1. Exception Recovery Patterns
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
CompletableFuture<String> future = CompletableFuture
    .supplyAsync(() -> {
        if (something) throw new RuntimeException("Error");
        return "result";
    })
    .exceptionally(ex -> {
        if (ex instanceof RuntimeException) {
            return "recovered";
        }
        throw new CompletionException(ex);
    })
    .whenComplete((result, ex) -> {
        // Log final outcome
        if (ex != null) {
            logger.error("Operation failed", ex);
        } else {
            logger.info("Operation succeeded with: " + result);
        }
    });
  1. Handling Checked Exceptions
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// WRONG - Checked exceptions must be wrapped
CompletableFuture.supplyAsync(() -> {
    throw new IOException(); // Won't compile
});

// CORRECT - Wrap checked exceptions
CompletableFuture.supplyAsync(() -> {
    try {
        throw new IOException();
    } catch (IOException e) {
        throw new CompletionException(e);
    }
});
  1. Global Exception Handler
1
2
3
4
// Set up global exception handler for uncaught exceptions
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
    logger.error("Uncaught async exception", throwable);
});

Combination

Two Tasks AND: thenCombine/thenAcceptBoth/thenAfterBoth

  1. These 3 methods are used to combine the results of 2 CompletableFutures then execute the 3rd task.

  2. thenCombine can get the results of both CompletableFutures and return a value.

  3. thenAcceptBoth can get the results of both CompletableFutures but does not return a value.

  4. thenAfterBoth does not get the results of both CompletableFutures and does not return a value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// Example 1: thenCombine
System.out.println("\n=== thenCombine Example ===");
CompletableFuture<Double> priceF = CompletableFuture.supplyAsync(() -> {
    System.out.println("Calculating price in thread: " + Thread.currentThread().getName());
    return 100.0;
}, executor);

CompletableFuture<Double> discountF = CompletableFuture.supplyAsync(() -> {
    System.out.println("Calculating discount in thread: " + Thread.currentThread().getName());
    return 0.2; // 20% discount
}, executor);

CompletableFuture<Double> finalPriceF = priceF.thenCombine(discountF, (price, discount) -> {
    System.out.println("Combining price and discount in thread: " + Thread.currentThread().getName());
    return price * (1 - discount);
});

System.out.println("Final price after discount: $" + finalPriceF.get());

// Example 2: thenAcceptBoth
System.out.println("\n=== thenAcceptBoth Example ===");
CompletableFuture<String> userF = CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching user in thread: " + Thread.currentThread().getName());
    return "John Doe";
}, executor);

CompletableFuture<Integer> orderCountF = CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching order count in thread: " + Thread.currentThread().getName());
    return 5;
}, executor);

userF.thenAcceptBoth(orderCountF, (user, orderCount) -> {
    System.out.println("Processing in thread: " + Thread.currentThread().getName());
    System.out.println("User " + user + " has placed " + orderCount + " orders");
}).get();

// Example 3: runAfterBoth
System.out.println("\n=== runAfterBoth Example ===");
CompletableFuture<Void> cacheUpdateF = CompletableFuture.runAsync(() -> {
    System.out.println("Updating cache in thread: " + Thread.currentThread().getName());
    System.out.println("Cache update completed");
}, executor);

CompletableFuture<Void> dbUpdateF = CompletableFuture.runAsync(() -> {
    System.out.println("Updating database in thread: " + Thread.currentThread().getName());
    System.out.println("Database update completed");
}, executor);

cacheUpdateF.runAfterBoth(dbUpdateF, () -> {
    System.out.println("Running final task in thread: " + Thread.currentThread().getName());
    System.out.println("All updates completed - sending notification");
}).get();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
=== thenCombine Example ===
Calculating discount in thread: pool-1-thread-2
Calculating price in thread: pool-1-thread-1
Combining price and discount in thread: main
Final price after discount: $80.0

=== thenAcceptBoth Example ===
Fetching user in thread: pool-1-thread-3
Fetching order count in thread: pool-1-thread-2
Processing in thread: main
User John Doe has placed 5 orders

=== runAfterBoth Example ===
Updating cache in thread: pool-1-thread-1
Cache update completed
Updating database in thread: pool-1-thread-3
Database update completed
Running final task in thread: main
All updates completed - sending notification

Two Tasks OR: applyToEither/acceptEither/runAfterEither

  1. These 3 methods are used to execute the 3rd task when either of the 2 CompletableFutures completes.

  2. applyToEither can get the result of the first completed CompletableFuture and return a value.

  3. acceptEither can get the result of the first completed CompletableFuture but does not return a value.

  4. runAfterEither does not get the result of the first completed CompletableFuture and does not return a value.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// Example 1: applyToEither - Get price from fastest responding service
System.out.println("\n=== applyToEither Example ===");
CompletableFuture<Double> service1Price = CompletableFuture.supplyAsync(() -> {
    System.out.println("Service 1 checking price in thread: " + Thread.currentThread().getName());
    sleep(2000); // Slower service
    return 100.50;
}, executor);

CompletableFuture<Double> service2Price = CompletableFuture.supplyAsync(() -> {
    System.out.println("Service 2 checking price in thread: " + Thread.currentThread().getName());
    sleep(1000); // Faster service
    return 99.99;
}, executor);

CompletableFuture<String> fastestPrice = service1Price.applyToEither(service2Price, price -> {
    System.out.println("Processing fastest price in thread: " + Thread.currentThread().getName());
    return String.format("Best available price: $%.2f", price);
});

System.out.println(fastestPrice.get());

// Example 2: acceptEither - Log first available weather data
System.out.println("\n=== acceptEither Example ===");
CompletableFuture<String> primaryWeather = CompletableFuture.supplyAsync(() -> {
    System.out.println("Primary weather service in thread: " + Thread.currentThread().getName());
    sleep(2000); // Slower service
    return "Sunny, 25°C";
}, executor);

CompletableFuture<String> backupWeather = CompletableFuture.supplyAsync(() -> {
    System.out.println("Backup weather service in thread: " + Thread.currentThread().getName());
    sleep(1000); // Faster service
    return "Sunny, 26°C";
}, executor);

primaryWeather.acceptEither(backupWeather, weather -> {
    System.out.println("Processing weather data in thread: " + Thread.currentThread().getName());
    System.out.println("Current weather: " + weather);
}).get();

// Example 3: runAfterEither - Notification after first cache update
System.out.println("\n=== runAfterEither Example ===");
CompletableFuture<Void> primaryCache = CompletableFuture.runAsync(() -> {
    System.out.println("Updating primary cache in thread: " + Thread.currentThread().getName());
    sleep(2000);
    System.out.println("Primary cache updated");
}, executor);

CompletableFuture<Void> backupCache = CompletableFuture.runAsync(() -> {
    System.out.println("Updating backup cache in thread: " + Thread.currentThread().getName());
    sleep(1000);
    System.out.println("Backup cache updated");
}, executor);

primaryCache.runAfterEither(backupCache, () -> {
    System.out.println("Running notification in thread: " + Thread.currentThread().getName());
    System.out.println("Cache system is now updated");
}).get();
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
=== applyToEither Example ===
Service 1 checking price in thread: pool-1-thread-1
Service 2 checking price in thread: pool-1-thread-2
Processing fastest price in thread: pool-1-thread-2
Best available price: $99.99

=== acceptEither Example ===
Primary weather service in thread: pool-1-thread-3
Backup weather service in thread: pool-1-thread-2
Processing weather data in thread: pool-1-thread-2
Current weather: Sunny, 26°C

=== runAfterEither Example ===
Updating primary cache in thread: pool-1-thread-1
Updating backup cache in thread: pool-1-thread-2
Backup cache updated
Running notification in thread: pool-1-thread-2
Cache system is now updated

Multiple Tasks: allOf/anyOf

  1. The allOf method is used to execute a task after all CompletableFutures complete.

  2. The anyOf method is used to execute a task after any CompletableFuture completes.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// Example 1: allOf with data aggregation
System.out.println("\n=== allOf Example - Product Details ===");
CompletableFuture<String> productDetails = CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching product details in thread: " + Thread.currentThread().getName());
    sleep(1000);
    return "Laptop XPS 15";
}, executor);

CompletableFuture<Double> productPrice = CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching price in thread: " + Thread.currentThread().getName());
    sleep(1500);
    return 1299.99;
}, executor);

CompletableFuture<Integer> productStock = CompletableFuture.supplyAsync(() -> {
    System.out.println("Checking stock in thread: " + Thread.currentThread().getName());
    sleep(1000);
    return 50;
}, executor);

CompletableFuture<Void> allDetails = CompletableFuture.allOf(
        productDetails, productPrice, productStock);

allDetails.thenRun(() -> {
    try {
        System.out.println("\nAll product information gathered:");
        System.out.println("Product: " + productDetails.get());
        System.out.println("Price: $" + productPrice.get());
        System.out.println("Stock: " + productStock.get() + " units");
    } catch (Exception e) {
        e.printStackTrace();
    }
}).get();

// Example 2: anyOf with multiple data sources
System.out.println("\n=== anyOf Example - Multiple Data Sources ===");
CompletableFuture<String> primaryDb = CompletableFuture.supplyAsync(() -> {
    System.out.println("Querying primary database in thread: " + Thread.currentThread().getName());
    sleep(2000); // Slow response
    return "Data from Primary DB";
}, executor);

CompletableFuture<String> secondaryDb = CompletableFuture.supplyAsync(() -> {
    System.out.println("Querying secondary database in thread: " + Thread.currentThread().getName());
    sleep(1000); // Faster response
    return "Data from Secondary DB";
}, executor);

CompletableFuture<String> cache = CompletableFuture.supplyAsync(() -> {
    System.out.println("Checking cache in thread: " + Thread.currentThread().getName());
    sleep(500); // Fastest response
    return "Data from Cache";
}, executor);

CompletableFuture<Object> firstResponse = CompletableFuture.anyOf(primaryDb, secondaryDb, cache);
System.out.println("First available response: " + firstResponse.get());
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
=== allOf Example - Product Details ===
Fetching product details in thread: pool-1-thread-1
Fetching price in thread: pool-1-thread-2
Checking stock in thread: pool-1-thread-3

All product information gathered:
Product: Laptop XPS 15
Price: $1299.99
Stock: 50 units

=== anyOf Example - Multiple Data Sources ===
Querying primary database in thread: pool-1-thread-4
Querying secondary database in thread: pool-1-thread-3
Checking cache in thread: pool-1-thread-1
First available response: Data from Cache

Summary

CompletableFuture is a powerful tool for asynchronous programming in Java. It allows developers to write non-blocking code and execute tasks in parallel. By understanding how to use CompletableFuture effectively, developers can improve the performance of their applications and create more responsive user experiences. This post has provided a comprehensive guide to using CompletableFuture in Java, including examples of how to use the various methods available.

Licensed under CC BY-NC-SA 4.0
Last updated on Dec 08, 2024 00:00 UTC
Built with Hugo
Theme Stack designed by Jimmy