Java / Java 21 Interview Questions
How does CompletableFuture work in Java and how does it relate to virtual threads?
CompletableFuture<T> (Java 8) provides composable asynchronous computation pipelines. It lets you chain transformations (thenApply), combinations (thenCombine), and error handlers (exceptionally) without blocking threads.
// Basic async computation
CompletableFuture future = CompletableFuture
.supplyAsync(() -> fetchUserName(userId)) // runs in ForkJoinPool
.thenApply(String::toUpperCase) // transform result
.thenApply(name -> "Hello, " + name);
System.out.println(future.join()); // blocks calling thread until done
// Combining two independent futures
CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture orderFuture = CompletableFuture.supplyAsync(() -> fetchOrder(id));
CompletableFuture result = userFuture
.thenCombine(orderFuture,
(user, order) -> user.name() + " ordered " + order.item());
// Error handling
future.exceptionally(ex -> "Default value on error: " + ex.getMessage())
.thenAccept(System.out::println);
// Run all, collect results
List> futures = ids.stream()
.map(id -> CompletableFuture.supplyAsync(() -> fetch(id)))
.toList();
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream().map(CompletableFuture::join).toList())
.join();
// With virtual threads (Java 21): use simple blocking code instead
// StructuredTaskScope is the recommended replacement for CompletableFuture
// when running on virtual threads — simpler and handles cancellation With Java 21 virtual threads, the async/reactive style of CompletableFuture is less necessary for I/O-bound work — you can write simple blocking code on virtual threads and achieve the same throughput. However, CompletableFuture remains useful for CPU-bound parallel pipelines and for code that must integrate with existing async APIs.
More Related questions...