-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
261: Completable Pipelines with CompletableFuture Chaining
You've likely used CompletableFuture to fire off a single task in the background. That's fine for simple things, but in a real production environment, we rarely just "do one thing" asynchronously. Usually, we have a sequence of dependent events: you fetch a user, then you use that user's ID to fetch their orders, and then you use those orders to calculate a loyalty discount.
If you do this naively with .join() or .get() at every step, you're essentially turning your asynchronous code back into synchronous code, blocking your threads and defeating the whole purpose. Instead, we build a pipeline. I'll show you how to do this by building a small "Travel Booking" sequence.
Defining our asynchronous building blocks
First, let's imagine we have three services: one for users, one for flights, and one for hotels. To keep this focused on the chaining, I'm going to simulate these as methods that return a CompletableFuture after a short delay. I'm using supplyAsync here because these would normally be network calls.
public CompletableFuture<String> fetchUser(String userId) {
return CompletableFuture.supplyAsync(() -> {
sleep(100); // Simulate network lag
return "User: " + userId + " (Premium Member)";
});
}
public CompletableFuture<String> findFlight(String user) {
return CompletableFuture.supplyAsync(() -> {
sleep(100);
return "Flight LH123 for " + user;
});
}
public CompletableFuture<String> bookHotel(String flight) {
return CompletableFuture.supplyAsync(() -> {
sleep(100);
return "Hotel Hilton booked for " + flight;
});
}
Connecting the dots with thenCompose
Now, here is where it gets interesting. I want to chain these together. If I use thenApply, I'm saying "Take the result of the first stage and transform it into something else." But since findFlight returns another CompletableFuture, using thenApply would give me a CompletableFuture<CompletableFuture<String>>. That's a nesting nightmare.
To flatten this, we use thenCompose. Think of it as the "flatMap" of the asynchronous world. It tells Java: "The next function returns a future; please wait for that future to complete and just give me the final result."
CompletableFuture<String> pipeline = fetchUser("Alice")
.thenCompose(user -> findFlight(user));
// Now 'pipeline' is a CompletableFuture<String> containing the flight info.
The 'Nested Future' mistake I almost made
I'll be honest—when I first started with these, I constantly reached for thenApply because it's more intuitive. I tried to add the hotel booking like this:
// WRONG WAY
CompletableFuture<CompletableFuture<String>> wrong = pipeline
.thenApply(flight -> bookHotel(flight));
I noticed immediately that the return type was wrong. I didn't want a future that contained another future; I just wanted the hotel confirmation. I had to realize that because bookHotel initiates its own asynchronous process, it must be composed, not applied. I corrected it back to thenCompose to keep the pipeline flat.
Closing the loop with thenAccept
Usually, the end of your pipeline isn't another async call, but an action—like logging the result or updating a UI. For this, we use thenAccept. This method consumes the final value and returns CompletableFuture<Void>, effectively signaling the end of the chain.
Here is the complete, corrected pipeline:
CompletableFuture<Void> travelBooking = fetchUser("Alice")
.thenCompose(user -> findFlight(user))
.thenCompose(flight -> bookHotel(flight))
.thenAccept(confirmation -> {
System.out.println("Booking Complete: " + confirmation);
});
// Since this is a demo, we block the main thread so the program doesn't exit
travelBooking.join();
Notice how the data flows naturally: User → Flight → Hotel → Console. No blocking calls happened until the very end, meaning the threads were free to do other work while the simulated network calls were pending.
📋 Practical Task
Build a Multi-Stage Order Fulfillment Pipeline
Create a small Java program that simulates an e-commerce order process using CompletableFuture chaining. You must implement the following logic without using .join() or .get() until the very end of the chain:
- Stage 1:
verifyInventory(String productId)- Returns aCompletableFuture<Boolean>indicating if the item is in stock. - Stage 2:
processPayment(Boolean inStock)- If in stock, returns aCompletableFuture<String>(the Transaction ID). If not in stock, it should complete exceptionally or return a "Payment Failed" message. - Stage 3:
generateShippingLabel(String transactionId)- Returns aCompletableFuture<String>containing the tracking number. - Stage 4: A final
thenAcceptblock that prints: "Order complete! Tracking number: [trackingNumber]".
Constraint: Ensure you use thenCompose for the stages that return new futures to avoid the "Nested Future" (CompletableFuture<CompletableFuture<T>>) problem.
There are no comments for now.