Componer Pipelines Asincronos con Java CompletableFuture
Construir pipelines async no bloqueantes en Java usando CompletableFuture con thenCompose, thenCombine, allOf, anyOf, manejo de excepciones, timeouts y thread pools personalizados.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripcion general
CompletableFuture es el primitivo async componible de Java. Encadena operaciones, combina multiples futures, maneja errores declarativamente y ejecuta en thread pools configurables. A continuacion: composicion secuencial con thenCompose, combinacion paralela con thenCombine y allOf, recuperacion de errores con exceptionally y handle, timeouts y executors personalizados.
Cuando Usar Esto
-
For alternatives, see Concurrent Async Tasks with asyncio.gather and Task Groups.
-
Llamadas a API a multiples servicios que necesitan agregacion
-
Pipelines async multi-paso (fetch → transform → persist)
-
Carga paralela de datos para vistas de dashboard o reportes
-
Reemplazar async basado en callbacks con pipelines componibles
Prerrequisitos
- Java 17+
java.net.http.HttpClient(Java 11+)
Solucion
1. CompletableFuture Basico
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class AsyncApiClient {
private static final HttpClient client = HttpClient.newHttpClient();
public static CompletableFuture<String> fetchAsync(String url) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.GET()
.build();
return client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(HttpResponse::body);
}
public static void main(String[] args) {
// No bloqueante — ejecuta en el executor del HttpClient
CompletableFuture<String> future = fetchAsync("https://api.example.com/users");
// Hacer otro trabajo mientras la peticion esta en vuelo...
// Bloquear solo cuando necesitas el resultado
String result = future.join();
System.out.println(result);
}
}
2. Composicion Secuencial con thenCompose
import java.util.concurrent.CompletableFuture;
public class SequentialPipeline {
public static CompletableFuture<User> fetchUser(String userId) {
return fetchAsync("https://api.example.com/users/" + userId)
.thenApply(json -> parseUser(json));
}
public static CompletableFuture<List<Order>> fetchOrders(String userId) {
return fetchAsync("https://api.example.com/users/" + userId + "/orders")
.thenApply(json -> parseOrders(json));
}
public static CompletableFuture<UserProfile> buildProfile(String userId) {
// thenCompose encadena futures — cada paso espera al anterior
return fetchUser(userId)
.thenCompose(user -> fetchOrders(user.getId())
.thenApply(orders -> new UserProfile(user, orders)));
}
public static void main(String[] args) {
UserProfile profile = buildProfile("123").join();
System.out.println(profile);
}
}
3. Combinacion Paralela con thenCombine
import java.util.concurrent.CompletableFuture;
public class ParallelCombination {
public static CompletableFuture<Dashboard> buildDashboard(String userId) {
// Los tres fetches ejecutan en paralelo
CompletableFuture<User> userFuture = fetchUser(userId);
CompletableFuture<List<Order>> ordersFuture = fetchOrders(userId);
CompletableFuture<List<Notification>> notifsFuture = fetchNotifications(userId);
// thenCombine merge dos futures — ejecuta despues de que ambos completan
return userFuture
.thenCombine(ordersFuture, (user, orders) ->
new PartialDashboard(user, orders))
.thenCombine(notifsFuture, (partial, notifs) ->
new Dashboard(partial.getUser(), partial.getOrders(), notifs));
}
// Tiempo total = max(fetchUser, fetchOrders, fetchNotifications), no suma
public static void main(String[] args) {
Dashboard dashboard = buildDashboard("123").join();
System.out.println(dashboard);
}
}
4. allOf — Esperar Todos los Futures
import java.util.concurrent.CompletableFuture;
import java.util.List;
import java.util.stream.Collectors;
public class AllOfPattern {
public static CompletableFuture<List<String>> fetchAllUrls(List<String> urls) {
// Iniciar todos los fetches en paralelo
List<CompletableFuture<String>> futures = urls.stream()
.map(AsyncApiClient::fetchAsync)
.collect(Collectors.toList());
// allOf retorna un CompletableFuture<Void> — completa cuando todos completan
CompletableFuture<Void> allDone = CompletableFuture.allOf(
futures.toArray(new CompletableFuture[0])
);
// Despues de que todos completan, recolectar resultados
return allDone.thenApply(v ->
futures.stream()
.map(CompletableFuture::join) // Seguro — todos estan completos
.collect(Collectors.toList())
);
}
public static void main(String[] args) {
List<String> urls = List.of(
"https://api.example.com/data/1",
"https://api.example.com/data/2",
"https://api.example.com/data/3"
);
List<String> results = fetchAllUrls(urls).join();
results.forEach(System.out::println);
}
}
5. anyOf — Primero en Completar
import java.util.concurrent.CompletableFuture;
public class AnyOfPattern {
// Retorna la primera respuesta exitosa — util para racing replicas
public static CompletableFuture<String> fetchFirstAvailable(List<String> urls) {
CompletableFuture<?>[] futures = urls.stream()
.map(AsyncApiClient::fetchAsync)
.toArray(CompletableFuture[]::new);
// anyOf completa cuando el primer future completa
return CompletableFuture.anyOf(futures)
.thenApply(obj -> (String) obj);
}
public static void main(String[] args) {
List<String> replicaUrls = List.of(
"https://replica1.example.com/data",
"https://replica2.example.com/data",
"https://replica3.example.com/data"
);
String result = fetchFirstAvailable(replicaUrls).join();
System.out.println("First response: " + result);
}
}
6. Manejo de Errores
import java.util.concurrent.CompletableFuture;
public class ErrorHandling {
public static CompletableFuture<String> fetchWithFallback(String url, String fallback) {
return fetchAsync(url)
// exceptionally — manejar errores, proporcionar fallback
.exceptionally(ex -> {
System.err.println("Fetch failed: " + ex.getMessage());
return fallback;
});
}
public static CompletableFuture<String> fetchWithRetry(String url, int maxRetries) {
return fetchAsync(url)
.handle((result, ex) -> {
if (ex != null) {
if (maxRetries > 0) {
System.err.println("Retrying (" + maxRetries + " left): " + ex.getMessage());
return fetchWithRetry(url, maxRetries - 1);
}
throw new RuntimeException("Max retries exceeded", ex);
}
return CompletableFuture.completedFuture(result);
})
.thenCompose(f -> f); // Aplanar CompletableFuture anidado
}
// whenComplete — side effects sin cambiar el resultado
public static CompletableFuture<String> fetchWithLogging(String url) {
return fetchAsync(url)
.whenComplete((result, ex) -> {
if (ex != null) {
log.error("Request to {} failed: {}", url, ex.getMessage());
} else {
log.info("Request to {} succeeded ({} bytes)", url, result.length());
}
});
}
public static void main(String[] args) {
String result = fetchWithFallback(
"https://api.example.com/maybe-down",
"{\"status\": \"fallback\"}"
).join();
System.out.println(result);
}
}
7. Thread Pool Personalizado
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CustomExecutorExample {
// Usar un pool dedicado — no depender del ForkJoinPool comun
private static final ExecutorService executor = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors(),
r -> {
Thread t = new Thread(r);
t.setName("async-worker-" + t.getId());
t.setDaemon(true);
return t;
}
);
public static CompletableFuture<String> fetchWithCustomPool(String url) {
// supplyAsync ejecuta en el executor especificado
return CompletableFuture.supplyAsync(() -> {
// Llamada bloqueante ejecuta en un thread worker, no en el thread principal
return blockingFetch(url);
}, executor);
}
public static CompletableFuture<UserProfile> buildProfileWithPool(String userId) {
return fetchUserWithPool(userId)
.thenComposeAsync(user -> fetchOrdersWithPool(user.getId()), executor)
.thenApplyAsync(orders -> new UserProfile(orders), executor);
}
public static void main(String[] args) {
try {
UserProfile profile = buildProfileWithPool("123").join();
System.out.println(profile);
} finally {
executor.shutdown();
}
}
}
8. Timeouts (Java 9+)
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class TimeoutExample {
public static CompletableFuture<String> fetchWithTimeout(String url) {
return fetchAsync(url)
.orTimeout(5, TimeUnit.SECONDS) // Completa excepcionalmente con TimeoutException
.exceptionally(ex -> {
if (ex instanceof java.util.concurrent.TimeoutException) {
return "{\"error\": \"request timed out\"}";
}
return "{\"error\": \"" + ex.getMessage() + "\"}";
});
}
// completeOnTimeout — proporcionar un valor default en lugar de una excepcion
public static CompletableFuture<String> fetchWithDefault(String url, String defaultValue) {
return fetchAsync(url)
.completeOnTimeout(defaultValue, 3, TimeUnit.SECONDS);
}
public static void main(String[] args) {
String result = fetchWithTimeout("https://slow-api.example.com/data").join();
System.out.println(result);
}
}
Como Funciona
- CompletableFuture: Representa una computacion async que producira un resultado. Puede ser completado manualmente o por una operacion async. Metodos como
thenApply,thenComposeythenCombineregistran callbacks que ejecutan cuando el future completa. thenApplyvsthenCompose:thenApplytoma una funcion sincrona (mapea el resultado).thenComposetoma una funcion que retorna otro CompletableFuture (aplana futures anidados — comoflatMapen programacion funcional).thenCombine: Merge dos futures independientes — el callback recibe ambos resultados. Ambos futures ejecutan en paralelo; el callback ejecuta despues de que ambos completan.allOf/anyOf:allOfretorna un future que completa cuando todos los futures de entrada completan.anyOfcompleta cuando el primer future de entrada completa. Ambos retornanCompletableFuture<Void>/CompletableFuture<Object>.- Callbacks async vs sync:
thenApplyejecuta el callback en el thread que completo el future.thenApplyAsynclo ejecuta en un thread separado (del executor). Usa variantes async para callbacks CPU-intensivos.
Variantes
Retry con Backoff Exponencial
public static CompletableFuture<String> fetchWithBackoff(
String url, int maxAttempts, long initialDelayMs) {
return fetchAsync(url)
.handle((result, ex) -> {
if (ex != null && maxAttempts > 0) {
long delay = initialDelayMs * (long) Math.pow(2, maxAttempts - 1);
return CompletableFuture.delayedExecutor(delay, TimeUnit.MILLISECONDS)
.supplyAsync(() -> fetchWithBackoff(url, maxAttempts - 1, initialDelayMs))
.thenCompose(f -> f);
}
return CompletableFuture.completedFuture(
ex != null ? null : result
);
})
.thenCompose(f -> f);
}
Combinar Resultados con Transformacion
public static CompletableFuture<AggregatedReport> buildReport(
String userId, String dateRange) {
CompletableFuture<SalesData> sales = fetchSales(userId, dateRange);
CompletableFuture<TrafficData> traffic = fetchTraffic(userId, dateRange);
CompletableFuture<RevenueData> revenue = fetchRevenue(userId, dateRange);
return CompletableFuture.allOf(sales, traffic, revenue)
.thenApply(v -> {
// Los tres estan completos — combinar resultados
return new AggregatedReport(
sales.join(),
traffic.join(),
revenue.join()
);
});
}
Mejores Practicas
- Siempre usar executors personalizados: Por defecto,
CompletableFutureejecuta en elForkJoinPool.commonPool(). Este pool es compartido en toda la JVM — una operacion lenta puede bloquear otras. Siempre pasa unExecutordedicado. - Usar
thenComposepara encadenado async:thenApplycon un tipo de retornoCompletableFuturecrea futures anidados. UsathenComposepara aplanarlos. - Siempre manejar excepciones: Las excepciones no manejadas en
CompletableFuturese swallow silenciosamente. Usaexceptionally,handleowhenCompletepara log y recuperacion. - Establecer timeouts: Sin timeouts, una operacion lenta bloquea
join()indefinidamente. UsaorTimeout()(Java 9+) ocompleteOnTimeout(). - Usar
allOfpara fan-out paralelo: Inicia todos los futures, luegoallOfpara esperar. Esto maximiza paralelismo — el tiempo total es el future mas lento, no la suma. - Evitar
join()en pipelines async:join()bloquea el thread actual. Usalo solo al final del pipeline. Dentro del pipeline, usathenComposeythenCombine.
Errores Comunes
- Usar el ForkJoinPool comun:
supplyAsyncsin un executor usa el pool comun compartido. Una operacion bloqueante puede starvear otras tareas. Siempre pasa un executor personalizado. - Anidar
thenApplycon futures:thenApply(f -> fetchAsync(...))creaCompletableFuture<CompletableFuture<T>>. UsathenComposeen su lugar para aplanar. - No manejar excepciones: Si un future completa excepcionalmente y nadie llama
exceptionallyohandle, la excepcion se pierde. Siempre agrega manejo de errores. - Bloquear con
join()en el pipeline:thenApply(f -> blockingCall())bloquea el thread del callback. UsathenApplyAsynccon un executor personalizado para callbacks bloqueantes. - No apagar los executors: Los executors personalizados mantienen threads vivos. Siempre
shutdown()en un bloquefinallyo via shutdown hook.
FAQ
Cual es la diferencia entre thenApply y thenCompose?
thenApply toma una Function<T, R> y retorna CompletableFuture<R>. thenCompose toma una Function<T, CompletableFuture<R>> y retorna CompletableFuture<R> — aplana futures anidados. Usa thenCompose cuando el callback retorna otro future.
Deberia usar thenApply o thenApplyAsync?
thenApply ejecuta el callback en el thread que completo el future (podria ser el thread del caller). thenApplyAsync lo ejecuta en un thread separado. Usa variantes async para callbacks CPU-intensivos o bloqueantes.
Como ejecuto multiples futures en paralelo?
Inicia todos los futures (ejecutan concurrentemente), luego usa allOf para esperar a todos. allOf retorna CompletableFuture<Void> — encadena thenApply para recolectar resultados.
Que pasa si un future en allOf falla?
allOf completa excepcionalmente si cualquier future falla. La excepcion es la primera encontrada. Usa handle en cada future para prevenir que un fallo aborte todo el batch.
Como establezco un timeout en un CompletableFuture?
Usa orTimeout(5, TimeUnit.SECONDS) (Java 9+) — el future completa excepcionalmente con TimeoutException. Usa completeOnTimeout(defaultValue, 5, TimeUnit.SECONDS) para proporcionar un valor fallback en su lugar.
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Concurrent Async Tasks with asyncio.gather and Task Groups
Execute multiple async operations concurrently in Python using asyncio.gather, asyncio.TaskGroup, error handling with return_exceptions, timeouts, and semaphores for rate limiting.
RecipeParallelize CPU and I/O Work with ThreadPoolExecutor
Use Python's ThreadPoolExecutor for parallel I/O operations, thread-safe result collection, Future callbacks, error handling, and mixing threads with asyncio for blocking work.
GuideConcurrency Patterns Guide
A guide to common concurrency patterns and what works for writing safe, efficient concurrent code.
GuideComplete Guide to GraphQL Federation
Build unified GraphQL APIs across multiple services with Apollo Federation. Covers subgraphs, supergraph composition, entity resolution, and gateway deployment.
RecipeBuild Async Pipelines with C# async/await and Task.Run
Build async pipelines in C# using async/await, Task.Run, Task.WhenAll, Task.WhenAny, CancellationTokenSource, Channels, and Parallel.ForEachAsync for concurrent I/O and CPU work.
RecipeConcurrent Patterns with Go Goroutines and Channels
Build concurrent systems in Go using goroutines, channels, select statements, worker pools, fan-out/fan-in, pipelines, context cancellation, and rate limiting with tickers.
RecipeScale Concurrent Applications with Java Virtual Threads
Scale Java applications with virtual threads from Project Loom. Use Thread.ofVirtual, Executors.newVirtualThreadPerTaskExecutor, structured concurrency, and scoped values.