20 Practical Examples of Using Java CompletableFuture for Asynchronous Programming

This article introduces Java's CompletableFuture and CompletionStage APIs, explains their contracts, and provides twenty concise code examples that demonstrate creating completed futures, running async stages, applying functions synchronously and asynchronously, handling exceptions, combining futures, using custom executors, and orchestrating multiple stages with allOf/anyOf.

Top Architect
Top Architect
Top Architect
20 Practical Examples of Using Java CompletableFuture for Asynchronous Programming

Java's CompletableFuture implements the CompletionStage interface and offers a powerful, standard‑library way to write asynchronous code without external frameworks such as RxJava.

Creating a completed future

static void completedFutureExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message");
    assertTrue(cf.isDone());
    assertEquals("message", cf.getNow(null));
}

Running a simple async stage

static void runAsyncExample() {
    CompletableFuture cf = CompletableFuture.runAsync(() -> {
        assertTrue(Thread.currentThread().isDaemon());
        randomSleep();
    });
    assertFalse(cf.isDone());
    sleepEnough();
    assertTrue(cf.isDone());
}

Applying a function to a previous stage

static void thenApplyExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message")
        .thenApply(s -> {
            assertFalse(Thread.currentThread().isDaemon());
            return s.toUpperCase();
        });
    assertEquals("MESSAGE", cf.getNow(null));
}

Applying a function asynchronously

static void thenApplyAsyncExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message")
        .thenApplyAsync(s -> {
            assertTrue(Thread.currentThread().isDaemon());
            randomSleep();
            return s.toUpperCase();
        });
    assertNull(cf.getNow(null));
    assertEquals("MESSAGE", cf.join());
}

Using a custom executor

static ExecutorService executor = Executors.newFixedThreadPool(3, new ThreadFactory() {
    int count = 1;
    @Override
    public Thread newThread(Runnable r) {
        return new Thread(r, "custom-executor-" + count++);
    }
});

static void thenApplyAsyncWithExecutorExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message")
        .thenApplyAsync(s -> {
            assertTrue(Thread.currentThread().getName().startsWith("custom-executor-"));
            assertFalse(Thread.currentThread().isDaemon());
            randomSleep();
            return s.toUpperCase();
        }, executor);
    assertNull(cf.getNow(null));
    assertEquals("MESSAGE", cf.join());
}

Consuming a previous result

static void thenAcceptExample() {
    StringBuilder result = new StringBuilder();
    CompletableFuture.completedFuture("thenAccept message")
        .thenAccept(s -> result.append(s));
    assertTrue("Result was empty", result.length() > 0);
}

Consuming a previous result asynchronously

static void thenAcceptAsyncExample() {
    StringBuilder result = new StringBuilder();
    CompletableFuture cf = CompletableFuture.completedFuture("thenAcceptAsync message")
        .thenAcceptAsync(s -> result.append(s));
    cf.join();
    assertTrue("Result was empty", result.length() > 0);
}

Completing a stage exceptionally

static void completeExceptionallyExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message")
        .thenApplyAsync(String::toUpperCase, CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS));
    CompletableFuture exceptionHandler = cf.handle((s, th) -> th != null ? "message upon cancel" : "");
    cf.completeExceptionally(new RuntimeException("completed exceptionally"));
    assertTrue(cf.isCompletedExceptionally());
    try { cf.join(); fail("Should have thrown an exception"); }
    catch (CompletionException ex) { assertEquals("completed exceptionally", ex.getCause().getMessage()); }
    assertEquals("message upon cancel", exceptionHandler.join());
}

Cancelling a computation

static void cancelExample() {
    CompletableFuture cf = CompletableFuture.completedFuture("message")
        .thenApplyAsync(String::toUpperCase, CompletableFuture.delayedExecutor(1, TimeUnit.SECONDS));
    CompletableFuture cf2 = cf.exceptionally(t -> "canceled message");
    assertTrue(cf.cancel(true));
    assertTrue(cf.isCompletedExceptionally());
    assertEquals("canceled message", cf2.join());
}

Applying a function to whichever of two futures completes first

static void applyToEitherExample() {
    String original = "Message";
    CompletableFuture cf1 = CompletableFuture.completedFuture(original)
        .thenApplyAsync(s -> delayedUpperCase(s));
    CompletableFuture cf2 = cf1.applyToEither(
        CompletableFuture.completedFuture(original).thenApplyAsync(s -> delayedLowerCase(s)),
        s -> s + " from applyToEither");
    assertTrue(cf2.join().endsWith(" from applyToEither"));
}

Combining results of two stages

static void thenCombineExample() {
    String original = "Message";
    CompletableFuture cf = CompletableFuture.completedFuture(original)
        .thenApply(s -> delayedUpperCase(s))
        .thenCombine(CompletableFuture.completedFuture(original).thenApply(s -> delayedLowerCase(s)),
            (s1, s2) -> s1 + s2);
    assertEquals("MESSAGEmessage", cf.getNow(null));
}

Composing dependent stages

static void thenComposeExample() {
    String original = "Message";
    CompletableFuture cf = CompletableFuture.completedFuture(original)
        .thenApply(s -> delayedUpperCase(s))
        .thenCompose(upper -> CompletableFuture.completedFuture(original)
            .thenApply(s -> delayedLowerCase(s))
            .thenApply(s -> upper + s));
    assertEquals("MESSAGEmessage", cf.join());
}

Running an action after any of several futures completes

static void anyOfExample() {
    StringBuilder result = new StringBuilder();
    List<String> messages = Arrays.asList("a", "b", "c");
    List<CompletableFuture> futures = messages.stream()
        .map(msg -> CompletableFuture.completedFuture(msg).thenApply(s -> delayedUpperCase(s)))
        .collect(Collectors.toList());
    CompletableFuture.anyOf(futures.toArray(new CompletableFuture[0]))
        .whenComplete((res, th) -> {
            if (th == null) { assertTrue(isUpperCase((String) res)); result.append(res); }
        });
    assertTrue(result.length() > 0);
}

Running an action after all futures complete (sync and async)

static void allOfExample() {
    StringBuilder result = new StringBuilder();
    List<String> messages = Arrays.asList("a", "b", "c");
    List<CompletableFuture> futures = messages.stream()
        .map(msg -> CompletableFuture.completedFuture(msg).thenApply(s -> delayedUpperCase(s)))
        .collect(Collectors.toList());
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .whenComplete((v, th) -> { futures.forEach(cf -> assertTrue(isUpperCase(cf.getNow(null)))); result.append("done"); });
    assertTrue(result.length() > 0);
}

static void allOfAsyncExample() {
    StringBuilder result = new StringBuilder();
    List<String> messages = Arrays.asList("a", "b", "c");
    List<CompletableFuture> futures = messages.stream()
        .map(msg -> CompletableFuture.completedFuture(msg).thenApplyAsync(s -> delayedUpperCase(s)))
        .collect(Collectors.toList());
    CompletableFuture all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .whenComplete((v, th) -> { futures.forEach(cf -> assertTrue(isUpperCase(cf.getNow(null)))); result.append("done"); });
    all.join();
    assertTrue(result.length() > 0);
}

Real‑world scenario: fetching a list of cars, rating each asynchronously, and processing when all are done

cars().thenCompose(cars -> {
    List<CompletionStage> updatedCars = cars.stream()
        .map(car -> rating(car.manufacturerId).thenApply(r -> { car.setRating(r); return car; }))
        .collect(Collectors.toList());
    CompletableFuture done = CompletableFuture.allOf(updatedCars.toArray(new CompletableFuture[updatedCars.size()]));
    return done.thenApply(v -> updatedCars.stream()
        .map(CompletionStage::toCompletableFuture)
        .map(CompletableFuture::join)
        .collect(Collectors.toList()));
}).whenComplete((cars, th) -> {
    if (th == null) { cars.forEach(System.out::println); }
    else { throw new RuntimeException(th); }
}).toCompletableFuture().join();

The article concludes with references to original sources and notes that the content is reproduced from various blogs and articles about Java CompletableFuture.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconcurrencyAsynchronousCompletableFutureCompletionStage
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.