Custom Implementation of CompletableFuture in Java
A Complete Step-by-Step Guide
Introduction
CompletableFuture in Java is a powerful tool for asynchronous programming.
It allows executing tasks in the background and processing results once they’re available — without blocking the main thread.
It’s part of the java.util.concurrent package and provides capabilities such as:
- Non-blocking asynchronous execution
- Task chaining
- Combining multiple futures
Building your own version helps you understand the core mechanics — how results are stored, how callbacks are triggered, and how chaining works.
Step 1 — Understand the Requirements
Before diving into code, let’s outline what our custom version needs to do.
A custom CompletableFuture must:
- Allow asynchronous task execution in a separate thread.
- Store the result for later retrieval.
- Support callbacks when computation completes.
- Support chaining or combining tasks sequentially.
Step 2 — Create Basic Structure
We start by creating a class CustomCompletableFuture<T> that will:
- Store the result
- Track completion status
- Allow registering callbacks
Code:
import java.util.function.Consumer;
public class CustomCompletableFuture<T> {
private T result;
private boolean isDone = false;
private Consumer<T> callback;
// Completes the future and triggers any callback
public synchronized void complete(T value) {
if (!isDone) {
result = value;
isDone = true;
notifyAll();
if (callback != null) {
callback.accept(result);
}
}
}
// Waits for the result if not yet available
public synchronized T get() throws InterruptedException {
while (!isDone) {
wait();
}
return result;
}
// Registers a callback to be executed when result is ready
public synchronized void thenAccept(Consumer<T> action) {
if (isDone) {
action.accept(result);
} else {
callback = action;
}
}
}
This class can store results, notify waiting threads, and execute callbacks when a task completes.
Step 3 — Implement Asynchronous Execution
We now add the ability to run tasks asynchronously.
To do that, we create a static method supplyAsync() that runs a task on a new thread.
Code:
public interface Task<U> {
U get();
}
public static <U> CustomCompletableFuture<U> supplyAsync(Task<U> task) {
CustomCompletableFuture<U> future = new CustomCompletableFuture<>();
new Thread(() -> {
U value = task.get();
future.complete(value);
}).start();
return future;
}
Now, you can execute a task asynchronously and receive a future to track its result.
Step 4 — Test CustomCompletableFuture
Let’s test the implementation with a simple asynchronous task.
Code:
public class CompletableFutureTest {
public static void main(String[] args) throws InterruptedException {
CustomCompletableFuture<String> future = CustomCompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Task Completed";
});
future.thenAccept(result -> System.out.println("Callback: " + result));
System.out.println("Main thread is not blocked.");
String result = future.get();
System.out.println("Result: " + result);
}
}
✅ Expected Output
Main thread is not blocked.
Callback: Task Completed
Result: Task Completed
Step 5 — Add Task Chaining Support
One of the main strengths of CompletableFuture is chaining tasks — executing one after another once the previous completes.
Let’s add that feature with a thenApply() method.
Code:
public <U> CustomCompletableFuture<U> thenApply(Task<U> nextTask) {
CustomCompletableFuture<U> nextFuture = new CustomCompletableFuture<>();
thenAccept(result -> {
U nextResult = nextTask.get();
nextFuture.complete(nextResult);
});
return nextFuture;
}
Now, you can run dependent asynchronous operations sequentially.
Step 6 — Test Task Chaining
Here’s a test for task chaining using our new thenApply() method.
Code:
public class CompletableFutureChainTest {
public static void main(String[] args) throws InterruptedException {
CustomCompletableFuture<Integer> future = CustomCompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});
future
.thenApply(() -> 20)
.thenAccept(result -> System.out.println("Chained Result: " + result));
System.out.println("Main thread continues...");
}
}
✅ Expected Output
Main thread continues...
Chained Result: 20
Step 7 — Add Exception Handling (Optional)
We can further enhance this by adding exception handling to manage runtime errors in asynchronous tasks.
This could include methods like:
- exceptionally() to handle exceptions gracefully.
- handle() to process results or errors.
However, for simplicity, this basic version focuses on the core logic of result handling, callbacks, and chaining.
Complete Code — Custom CompletableFuture Example
Here’s the entire working implementation you can copy and run:
import java.util.function.Consumer;
// Step 1 & 2 — Custom CompletableFuture Implementation
public class CustomCompletableFuture<T> {
private T result;
private boolean isDone = false;
private Consumer<T> callback;
public synchronized void complete(T value) {
if (!isDone) {
result = value;
isDone = true;
notifyAll();
if (callback != null) {
callback.accept(result);
}
}
}
public synchronized T get() throws InterruptedException {
while (!isDone) {
wait();
}
return result;
}
public synchronized void thenAccept(Consumer<T> action) {
if (isDone) {
action.accept(result);
} else {
callback = action;
}
}
// Step 3 — Asynchronous Execution
public interface Task<U> {
U get();
}
public static <U> CustomCompletableFuture<U> supplyAsync(Task<U> task) {
CustomCompletableFuture<U> future = new CustomCompletableFuture<>();
new Thread(() -> {
U value = task.get();
future.complete(value);
}).start();
return future;
}
// Step 5 — Task Chaining
public <U> CustomCompletableFuture<U> thenApply(Task<U> nextTask) {
CustomCompletableFuture<U> nextFuture = new CustomCompletableFuture<>();
thenAccept(result -> {
U nextResult = nextTask.get();
nextFuture.complete(nextResult);
});
return nextFuture;
}
}
// Step 4 — Test Simple Future
class CompletableFutureTest {
public static void main(String[] args) throws InterruptedException {
CustomCompletableFuture<String> future = CustomCompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Task Completed";
});
future.thenAccept(result -> System.out.println("Callback: " + result));
System.out.println("Main thread is not blocked.");
String result = future.get();
System.out.println("Result: " + result);
}
}
// Step 6 — Test Task Chaining
class CompletableFutureChainTest {
public static void main(String[] args) throws InterruptedException {
CustomCompletableFuture<Integer> future = CustomCompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 10;
});
future
.thenApply(() -> 20)
.thenAccept(result -> System.out.println("Chained Result: " + result));
System.out.println("Main thread continues...");
}
}
Real-World Use Cases
Custom CompletableFuture can be useful in:
- Running background tasks without blocking the main thread
- Chaining dependent asynchronous operations
- Building lightweight reactive systems
- rk or database calls in the background
Limitations
While educational, this version lacks:
- Advanced combinators like thenCombine(), allOf(), or anyOf().
- Timeout handling and cancellation support.
- Proper error propagation.
In real-world production, you should use Java’s built-in CompletableFuture, which provides all these features safely and efficiently.
Summary
- CompletableFuture helps manage asynchronous tasks elegantly.
- Building a CustomCompletableFuture improves understanding of:
- Threads and synchronization
- Result propagation
- Callback mechanisms
- Task chaining logic
This custom version lays the foundation for mastering asynchronous and reactive programming in Java.
Next Blog- Custom Implementation of LinkedBlockingQueue in Java
