Back to CompletableFuture & Resilience
Q17Question 17 of 19

How to race on first successful result?

Key Concepts
  • anyOf() completes on the first completion, which may be a failure, not the first success.
  • Race-on-success: launch N parallel requests, return the first successful result, ignore failures, cancel the rest.
  • Implement with a result CF, whenComplete callbacks, an atomic failure counter, and orTimeout for a deadline.
  • Cancel remaining futures on first success to avoid wasting backend resources.
  • Trade-off: N parallel calls equals N times the backend load, justified only for latency-critical paths.
  • Three outcomes: first success (return it), all failed (return last error), timeout (TimeoutException).

Definition

anyOf() returns the first completion which may be a failure; the race-on-success pattern returns the first successful result, ignores failures, cancels remaining futures, and enforces a timeout deadline.
  • anyOf() pitfall – completes on the first completion, which may be a failure (fast error beats slow success).
  • Race-on-success – launch N parallel requests, complete a result CF on the first success, ignore individual failures.
  • Cancel remaining – when one succeeds, cancel the other futures to save backend resources.
  • Timeout – orTimeout() on the result CF enforces a deadline for the entire race.
  • Trade-off – N parallel calls = N times backend load; justified for latency-critical, revenue-impacting paths.
  • Three outcomes – first success (return value), all failed (last error), timeout (TimeoutException).

You need Rajesh's credit score fast (500ms deadline). You call three bureaus simultaneously (hedged request). Bureau-1 errors in 10ms. Bureau-2 responds in 200ms. Bureau-3 responds in 400ms. anyOf returns Bureau-1's error (first to complete, but a failure). Race-on-success ignores Bureau-1's error, returns Bureau-2's success at 200ms, and cancels Bureau-3.

Understanding the Concepts

What is the anyOf pitfall?

anyOf completes on the first completion, but that may be a failure rather than a success. If one backend errors fast while another would succeed slowly, anyOf returns the error.

JAVA
package com.bank.resilience;

import java.util.concurrent.CompletableFuture;

public class AnyOfPitfallDemo {
    public static void main(String[] args) {
        CompletableFuture<Object> first = CompletableFuture.anyOf(
            CompletableFuture.supplyAsync(() -> {
                throw new RuntimeException("Bureau-1 down!");
            }),
            CompletableFuture.supplyAsync(() -> {
                sleep(200); return 750;  // would succeed
            }),
            CompletableFuture.supplyAsync(() -> {
                sleep(400); return 720;  // would succeed
            })
        );
        // anyOf returns Bureau-1's FAILURE (first to complete!)
        // Bureau-2 would have succeeded at 200ms, but anyOf already completed.
    }
}

How do you implement race-on-success?

The race-on-success pattern uses a shared result CompletableFuture, whenComplete callbacks on each candidate, and an atomic failure counter to detect when all candidates have failed.

JAVA
package com.bank.service;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.List;

public class RaceOnSuccess {

    public static <T> CompletableFuture<T> race(
            List<CompletableFuture<T>> futures, long timeoutMs) {

        CompletableFuture<T> result = new CompletableFuture<>();
        AtomicInteger remaining = new AtomicInteger(futures.size());

        for (CompletableFuture<T> cf : futures) {
            cf.whenComplete((value, ex) -> {
                if (ex == null) {
                    // First success wins
                    if (result.complete(value)) {
                        // Cancel remaining futures to save resources
                        futures.forEach(f -> f.cancel(true));
                    }
                } else {
                    // Failure: decrement counter
                    if (remaining.decrementAndGet() == 0 && !result.isDone()) {
                        // All failed: complete exceptionally with last error
                        result.completeExceptionally(ex);
                    }
                }
            });
        }

        // Enforce a deadline on the entire race
        result.orTimeout(timeoutMs, TimeUnit.MILLISECONDS);
        return result;
    }
}

How do you use it in a banking credit score lookup?

JAVA
package com.bank.credit;

import java.util.List;
import java.util.concurrent.*;
import java.math.BigDecimal;

public class CreditScoreService {

    private final ExecutorService bureauPool = Executors.newFixedThreadPool(9);

    public CompletableFuture<Integer> fetchFastestScore(String customerId) {
        List<CompletableFuture<Integer>> calls = List.of(
            CompletableFuture.supplyAsync(
                () -> callBureau1(customerId), bureauPool),
            CompletableFuture.supplyAsync(
                () -> callBureau2(customerId), bureauPool),
            CompletableFuture.supplyAsync(
                () -> callBureau3(customerId), bureauPool)
        );
        return RaceOnSuccess.race(calls, 500);  // 500ms deadline
    }
}

What is the trade-off?

N parallel calls means N times the backend load. This is a deliberate latency-vs-load trade-off. At National Bank of India, three credit bureau calls in parallel with a 500ms deadline reduced P99 from 2000ms to 300ms, improving loan conversion by 15%. The 3x bureau cost was worth the revenue impact.

This is justified for latency-critical, revenue-impacting paths like checkout pages, loan approval, and real-time pricing. It is not justified for background batch processing where latency does not matter.

What are the three outcomes?

| Outcome | What happens | Example |
|---|---|---|
| First success | Result CF completed, others cancelled | Bureau-2 responds at 200ms |
| All failed | Result CF completed exceptionally with last error | All 3 bureaus down |
| Timeout | Result CF completed with TimeoutException | None responds within 500ms |

Does cancel(true) actually stop the running task?

cancel(true) sets the interrupt flag on the thread running the task. If the task is in a blocking call (Thread.sleep, IO), an InterruptedException is thrown. If the task is in a computation loop, it must cooperate by checking Thread.currentThread().isInterrupted(). For HTTP calls, combine with HTTP client-level timeouts for reliable cancellation.

What are common interview follow-up questions?

  • When is hedged requesting worth the cost? When P99 latency directly impacts revenue: checkout, search, ads, real-time pricing.
  • How does this compare to Resilience4j Retry? Retry is sequential (try, wait, try again). Hedging is parallel (try all simultaneously). Retry is cheaper on backend load, hedging is faster on latency.
  • How do you limit the blast radius? Use a dedicated bounded pool for hedged calls so they cannot starve other work.

Visual Overview

DIAGRAM
  Race-on-Success Pattern

  ┌──────────┐  ┌──────────┐  ┌──────────┐
  │ Bureau-1 │  │ Bureau-2 │  │ Bureau-3 │
  │ FAILS    │  │ 200ms    │  │ 400ms    │
  │ (10ms)   │  │ score=750│  │ score=720│
  └────┬─────┘  └────┬─────┘  └────┬─────┘
       │              │              │
       ▼              ▼              │
  Ignored        result.complete(750)│
  (failure)      Cancel Bureau-3 ────┘

  anyOf: returns Bureau-1 error (WRONG)
  race:  returns Bureau-2 score (CORRECT)

Key Takeaway

anyOf() completes on the first completion, which may be a failure. If one backend fails fast (10ms error) while another would succeed (200ms), anyOf returns the failure. The race-on-success pattern fixes this by completing a result CF on the first success, ignoring individual failures, cancelling remaining futures, and enforcing a timeout deadline.

The trade-off is N parallel calls for N times the backend load. This is a deliberate latency-vs-load trade-off justified for revenue-critical paths. At National Bank of India, three credit bureau calls in parallel with a 500ms deadline reduced P99 from 2000ms to 300ms, improving loan conversion by 15%. The 3x bureau cost was worth the revenue impact.

Remember one sentence: anyOf may return a failure; race-on-success returns the first success and cancels the rest.

---

Share
Use ← → arrow keys to navigate