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:

  1. Is it required or optional?
  2. What is its deadline?
  3. What executor should run it?
  4. What happens on timeout or failure?
  5. 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.
  • CompletableFuture works 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