Service aggregation is where async composition stops being an academic API topic and becomes real backend architecture.
A single endpoint may need data from:
- profile service
- pricing service
- inventory service
- recommendations service
The orchestration problem is not just “run these concurrently.” It is:
- run them concurrently with deadlines, fallbacks, and clear dependency structure
That is exactly where CompletableFuture can help.
Common Aggregation Shapes
Parallel fan-out and fan-in
Start several independent calls together, then combine the results.
Optional dependency fallback
Some data is helpful but not required, so timeouts or failures should degrade gracefully.
Dependent follow-up
One call’s result determines the next request, which makes composition order important.
First-response-wins
Useful for redundant replicas or speculative fallback patterns.
These are not all the same workflow. That is why operator choice matters.
Runnable Example
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class ServiceAggregationDemo {
public static void main(String[] args) {
ExecutorService ioExecutor = Executors.newFixedThreadPool(16);
CompletableFuture<String> profileFuture =
CompletableFuture.supplyAsync(() -> loadProfile("u-42"), ioExecutor);
CompletableFuture<String> inventoryFuture =
CompletableFuture.supplyAsync(() -> loadInventory("sku-99"), ioExecutor)
.completeOnTimeout("inventory-unavailable", 200, TimeUnit.MILLISECONDS);
CompletableFuture<String> pricingFuture =
CompletableFuture.supplyAsync(() -> loadPricing("sku-99"), ioExecutor)
.orTimeout(200, TimeUnit.MILLISECONDS);
CompletableFuture<String> responseFuture =
profileFuture.thenCombine(inventoryFuture, (profile, inventory) ->
profile + " / " + inventory)
.thenCombine(pricingFuture, (partial, pricing) ->
partial + " / " + pricing);
try {
System.out.println(responseFuture.join());
} finally {
ioExecutor.shutdown();
}
}
static String loadProfile(String userId) {
return "profile:" + userId;
}
static String loadInventory(String sku) {
return "inventory:" + sku;
}
static String loadPricing(String sku) {
return "price:" + sku;
}
}
The important design ideas here are:
- independent fan-out
- explicit timeout policy
- fallback on optional data
- clear combination points
Design Questions That Matter
For each dependency, ask:
- Is it required or optional?
- What is its deadline?
- What executor should run it?
- What happens on timeout or failure?
- Should its result be combined, transformed, or ignored?
These are orchestration questions first. The API only expresses the answers.
Common Mistakes
Starting dependent calls sequentially when they could run in parallel
This wastes latency budget.
Treating all dependencies as equally critical
That often causes total request failure when graceful degradation would be better.
Joining intermediate futures too early
That breaks concurrency and turns orchestration back into serial blocking.
Ignoring total request deadline
Per-stage timeouts are not enough if the overall response time budget is still uncontrolled.
Production Guidance
Healthy aggregation workflows usually include:
- clear dependency classification
- bounded executor usage
- per-dependency deadlines
- fallback where business semantics allow it
- metrics for timeout rate, fallback rate, and dependency latency
The design goal is not maximum parallelism. It is controlled latency and graceful behavior when dependencies misbehave.
Choose the Pattern by Failure Semantics
Aggregation patterns look similar on a diagram, but they behave very differently when dependencies are slow or broken. That is the part readers need to internalize. A required pricing call and an optional recommendations call should not sit in the same orchestration path with the same timeout and failure policy just because both are remote requests.
Choose the orchestration shape according to what failure means:
- required dependency: fail fast or propagate a clear error
- optional dependency: degrade with fallback or omission
- redundant dependency: first acceptable answer wins
- dependent call chain: stop the chain early when a prerequisite fails
Once those semantics are clear, the CompletableFuture operators become much easier to choose.
Observability and Testing Notes
Aggregation logic needs tests that reflect service behavior, not just fluent API correctness. Write scenarios for:
- one required dependency timing out
- one optional dependency degrading successfully
- multiple dependencies failing together
- slow completion after the client-visible deadline has already passed
Also instrument the workflow with enough detail to answer incident questions quickly: which dependency timed out, which path fell back, and how much latency each stage consumed. That is what makes async orchestration operable rather than merely clever.
Second Example: First Response Wins
Aggregation is not always “wait for everyone.” A second scenario worth seeing is redundant lookup where the fastest acceptable answer wins.
import java.util.concurrent.CompletableFuture;
public class ReplicaRaceDemo {
public static void main(String[] args) {
CompletableFuture<String> primary = CompletableFuture.supplyAsync(() -> "primary-price");
CompletableFuture<String> replica = CompletableFuture.supplyAsync(() -> "replica-price");
CompletableFuture<Object> fastest = CompletableFuture.anyOf(primary, replica);
System.out.println(fastest.join());
}
}
This is a very different orchestration pattern from fan-out and combine:
- multiple providers race
- only the first acceptable result matters
- the composition method should reflect that workflow shape
Key Takeaways
- Async aggregation is about dependency structure, deadlines, and fallback policy, not just starting many futures.
CompletableFutureworks well for fan-out/fan-in flows when operator choice matches the dependency graph.- Optional and required dependencies should not share the same failure strategy by default.
- The best async aggregation design optimizes for bounded latency and graceful degradation, not just raw concurrency.
Next post: How to Test Concurrent Code Without Fooling Yourself
Comments