Asynchronous programming is essential for building responsive, high-performance applications. This post explores how Java, Scala, and Kotlin handle async operations, with a focus on aggregating data from multiple APIs concurrently.
The Problem
Imagine building a service that aggregates data from multiple external APIs:
- Weather API
- Traffic API
- News API
We need to:
- Call all APIs concurrently (not sequentially)
- Handle timeouts gracefully
- Provide fallback values when APIs fail
- Combine results into an aggregated response
Basic Async Operations
// Create an async task - similar to Scala's Future { ... }
CompletableFuture<ApiResponse> fetchWeatherData() {
return CompletableFuture.supplyAsync(() -> {
// Simulate API call
Thread.sleep(150);
return ApiResponse.of("weather", "{\"temp\": 22}");
}, executor);
}
def fetchWeatherData(): Future[ApiResponse] = Future {
simulateApiCall("Weather API", 150)
ApiResponse("weather", """{"temp": 22}""")
}
fun CoroutineScope.fetchWeatherDataAsync(): Deferred<ApiResponse> =
async(Dispatchers.IO) {
simulateApiCall("Weather API", 150)
ApiResponse.of("weather", """{"temp": 22}""")
}
Transformation and Chaining
// Transform the result (like Scala's map)
CompletableFuture<String> transformResponse(CompletableFuture<ApiResponse> future) {
return future.thenApply(response -> response.data().toUpperCase());
}
// Chain dependent async operations (like Scala's flatMap)
CompletableFuture<ApiResponse> fetchAndEnrichWeather() {
return fetchWeatherData()
.thenCompose(weather -> enrichWithLocation(weather));
}
// Transform the result (map)
def transformResponse(future: Future[ApiResponse]): Future[String] =
future.map(response => response.data.toUpperCase)
// Chain with for-comprehension (flatMap)
def fetchAndEnrichWeather(): Future[ApiResponse] =
for
weather <- fetchWeatherData()
enriched <- enrichWithLocation(weather)
yield enriched
// Sequential operations (looks synchronous but is async!)
suspend fun fetchAndEnrichWeather(): ApiResponse {
val weather = fetchWeatherDataAsync().await()
return enrichWithLocation(weather)
}
Combining Multiple Futures
// Wait for all futures to complete
CompletableFuture<AggregatedData> aggregateFromAllApis(Duration timeout) {
var weather = fetchWeatherData()
.completeOnTimeout(fallback, timeout.toMillis(), TimeUnit.MILLISECONDS);
var traffic = fetchTrafficData()
.completeOnTimeout(fallback, timeout.toMillis(), TimeUnit.MILLISECONDS);
var news = fetchNewsData()
.completeOnTimeout(fallback, timeout.toMillis(), TimeUnit.MILLISECONDS);
return CompletableFuture.allOf(weather, traffic, news)
.thenApply(v -> AggregatedData.success(List.of(
weather.join(),
traffic.join(),
news.join()
)));
}
def aggregateFromAllApis(timeout: FiniteDuration): Future[AggregatedData] =
val futures = List(
withTimeout(fetchWeatherData(), timeout, fallback),
withTimeout(fetchTrafficData(), timeout, fallback),
withTimeout(fetchNewsData(), timeout, fallback)
)
Future.sequence(futures).map(responses =>
AggregatedData.success(responses)
)
suspend fun aggregateFromAllApis(timeout: Duration): AggregatedData =
coroutineScope {
val weather = async { fetchWithTimeout("weather", timeout) }
val traffic = async { fetchWithTimeout("traffic", timeout) }
val news = async { fetchWithTimeout("news", timeout) }
val responses = listOf(weather, traffic, news).awaitAll()
AggregatedData.success(responses)
}
Racing Futures
// Get the first result
CompletableFuture<ApiResponse> getFirstAvailableResponse() {
return CompletableFuture.anyOf(
fetchWeatherData(),
fetchTrafficData(),
fetchNewsData()
).thenApply(result -> (ApiResponse) result);
}
def getFirstAvailableResponse(): Future[ApiResponse] =
Future.firstCompletedOf(List(
fetchWeatherData(),
fetchTrafficData(),
fetchNewsData()
))
suspend fun getFirstAvailableResponse(): ApiResponse =
coroutineScope {
val weather = fetchWeatherDataAsync()
val traffic = fetchTrafficDataAsync()
val news = fetchNewsDataAsync()
select {
weather.onAwait { it }
traffic.onAwait { it }
news.onAwait { it }
}
}
Error Handling
// Recovery (like Scala's recover)
future.exceptionally(ex -> ApiResponse.of("fallback", "{}"));
// Full handling (like Scala's transform)
future.handle((response, ex) -> {
if (ex != null) return "Error: " + ex.getMessage();
return "Success: " + response.data();
});
// Timeout handling (Java 9+)
future.completeOnTimeout(fallback, 5, TimeUnit.SECONDS);
future.orTimeout(5, TimeUnit.SECONDS);
// Recovery
future.recover { case ex: Exception =>
ApiResponse("fallback", "{}")
}
// Full transformation
future.transform {
case Success(response) => Success(s"Success: ${response.data}")
case Failure(ex) => Success(s"Error: ${ex.getMessage}")
}
// Using runCatching (Kotlin's Result type)
suspend fun fetchWithHandling(): String {
val result = runCatching { fetchApiData() }
return result.fold(
onSuccess = { "Success: ${it.data}" },
onFailure = { "Error: ${it.message}" }
)
}
// Built-in timeout handling
val result = withTimeoutOrNull(5.seconds) {
fetchSlowApi()
} ?: fallbackValue
Comparison Table
| Feature | Java CompletableFuture | Scala Future | Kotlin Coroutines |
|---|---|---|---|
| Create async | supplyAsync() |
Future { } |
async { } |
| Transform | thenApply() |
map |
await() + transform |
| Chain | thenCompose() |
flatMap / for-comp |
Sequential await() |
| Combine all | allOf() |
sequence |
awaitAll() |
| First completed | anyOf() |
firstCompletedOf |
select { } |
| Timeout fallback | completeOnTimeout() |
Helper needed | withTimeoutOrNull() |
| Timeout exception | orTimeout() |
Helper needed | withTimeout() |
| Recover | exceptionally() |
recover |
try/catch |
| Transform both | handle() |
transform |
runCatching().fold() |
Key Insights for Scala Developers
-
Java’s API is verbose but complete: CompletableFuture has everything you need, including built-in timeout methods (Java 9+).
-
Kotlin coroutines feel natural: The suspend function model makes async code look synchronous, which can be easier to read and maintain.
-
Scala’s for-comprehensions are elegant: Nothing beats the readability of Scala’s for-comprehension syntax for chaining futures.
- Error handling differs:
- Java uses
exceptionally()andhandle() - Scala uses
recoverandtransform - Kotlin uses standard try/catch with suspend functions
- Java uses
- Timeout handling: Java 9+ has built-in timeout methods, while Scala requires custom helpers. Kotlin has excellent built-in support.
Full Working Examples
Check out the complete implementation in our repository:
Interview Q&A: CompletableFuture in Practice
What is CompletableFuture used for?
What is the difference between thenApply() and thenCompose()?
thenApply() transforms the result of a future when it is ready. thenCompose() is for when the transformation itself produces another future. In plain English, one handles a value, and the other handles a whole async step that may itself still be running.
How do you combine multiple futures?
allOf() or anyOf(). allOf() waits for everything to finish, while anyOf() continues when the first one completes. These patterns are very common in service code that fetches data from several APIs at the same time.
What is the difference between exceptionally() and handle()?
handle() gets called for both success and failure. exceptionally() only runs when the future fails. So handle() is a bit more general and is useful when you want to deal with both outcomes in one place.
How is this related to Scala futures or Kotlin coroutines?
CompletableFuture is still a powerful and common tool when you need strong async composition in Java code.
Conclusion
All three languages provide powerful async primitives. For Scala developers moving to Java:
- Think of
supplyAsync()asFuture { } - Think of
thenApply()asmap - Think of
thenCompose()asflatMap - Think of
allOf()asFuture.sequence - Think of
exceptionally()asrecover
The patterns are similar, just with different syntax. The key is understanding the mental model: futures represent values that will be available in the future, and all three languages let you compose them elegantly.
Happy async coding! 🚀
This post is part of the Java 21 Interview Preparation Guide - Your Roadmap to Success. Next related posts: Stream API Advanced Operations and Virtual Threads and Structured Concurrency.