Definition
- Fan-out amplification – N requests per second, each fanning out to K calls via
allOf(), creates N x K downstream requests per second. - Self-inflicted DDoS – overwhelming your own dependencies by parallelizing without safeguards.
- Bulkhead – bounded thread pool per downstream limits concurrent calls.
- Rate limiter – caps outbound calls per second (Resilience4j
RateLimiter). - Timeout –
orTimeout()prevents threads from blocking forever. - Circuit breaker – stops calling a failing dependency; fast-fails for a cooldown period.
- Connection pool – HikariCP
maxPoolSizemust handle N x K concurrent connections.
National Bank of India's branch (service) serves 1000 customers per hour. Each customer needs 3 checks: credit bureau, property valuation, income verification. The branch sends 3000 calls per hour to these agencies. If the credit bureau can only handle 500 per hour, it is overwhelmed, rejects requests, and your customers fail. You just DDoS'd your own partner.
Understanding the Concepts
What is fan-out amplification?
Fan-out amplification occurs when each incoming request triggers multiple downstream calls. If your service receives N requests per second and each fans out to K downstream calls, the total downstream load is N x K.
package com.bank.loan;
import java.util.concurrent.CompletableFuture;
public class LoanApprovalService {
// Service receives 1000 req/s
// Each request fans out to 3 CFs:
public void approveLoan(String customerId) {
CompletableFuture.allOf(
CompletableFuture.supplyAsync(() -> callCreditBureau(customerId), ioPool),
CompletableFuture.supplyAsync(() -> callPropertyAPI(customerId), ioPool),
CompletableFuture.supplyAsync(() -> callIncomeVerify(customerId), ioPool)
).join();
// Total downstream: 3000 calls/s!
}
}
Why is this a self-inflicted DDoS?
Without safeguards, your service overwhelms its own dependencies. If the credit bureau can handle 500 requests per second but you send 1000, 50% of requests fail. The failing requests cause retries, which amplify the load further. This feedback loop can bring down the entire dependency chain.
What safeguards are required?
Five safeguards per downstream dependency, applied as a stack.
| Safeguard | Purpose | Example |
|---|---|---|
| Bulkhead | Limit concurrent calls | 20-thread pool per downstream |
| Rate limiter | Cap calls per second | 500/s to credit bureau |
| Timeout | Prevent thread hang | orTimeout(5, SECONDS) |
| Circuit breaker | Stop calling on failure | Open at 50% failure rate |
| Connection pool | Size for concurrent load | HikariCP maxPoolSize=200 |
How do they work together in code?
package com.bank.resilience;
import java.util.concurrent.*;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.ratelimiter.RateLimiter;
public class SafeDownstreamCall {
private final ExecutorService creditBureauPool =
Executors.newFixedThreadPool(20); // bulkhead: bounded pool
private final CircuitBreaker circuitBreaker =
CircuitBreaker.ofDefaults("creditBureau");
private final RateLimiter rateLimiter =
RateLimiter.ofDefaults("creditBureau");
public CompletableFuture<Integer> fetchCreditScore(String customerId) {
return CompletableFuture.supplyAsync(() -> {
// 3. Circuit breaker: fast-fail if downstream is unhealthy
return circuitBreaker.executeSupplier(() ->
// 2. Rate limiter: caps calls/second
rateLimiter.executeSupplier(() ->
callCreditBureau(customerId)
)
);
}, creditBureauPool) // 1. Bulkhead: bounded pool limits concurrency
.orTimeout(5, TimeUnit.SECONDS); // 4. Timeout: prevents thread hang
}
}
How do you size connection pools for fan-out?
The formula is: maxPoolSize >= maxConcurrentRequests x fanOutFactor x avgLatency / 1000. If you have 200 concurrent requests, each fanning out to 3 calls, and each call takes 100ms, you need at least 200 x 3 x 0.1 = 60 concurrent connections to the downstream. Add headroom for spikes.
What happens without safeguards?
1000 req/s x 3 fan-out = 3000 downstream calls/s
Credit bureau capacity: 500/s
Result: 83% rejected, cascade failure
HikariCP pool: 20 connections, 1000 concurrent requests
Result: connection wait timeout errors, queue grows, OOM
No timeout: hung calls block threads forever
Result: pool exhausted, entire service down
With safeguards:
Bulkhead: max 20 concurrent credit bureau calls
Rate limiter: max 500/s to credit bureau
Timeout: 5s max per call
Circuit breaker: fast-fail after 50% failure rate
Result: service degrades gracefully, stays alive
What is the difference between a circuit breaker and a rate limiter?
A circuit breaker reacts to failures. It monitors the error rate and when it exceeds a threshold, it opens the circuit and fast-fails all subsequent calls for a cooldown period. A rate limiter prevents overload proactively by capping the number of calls per time window regardless of success or failure.
What are common interview follow-up questions?
- How does Resilience4j combine with CompletableFuture? Resilience4j provides decorators that wrap suppliers with bulkhead, circuit breaker, rate limiter, retry, and timeout.
- How do you monitor fan-out? Expose metrics per downstream: concurrent calls, error rate, latency percentiles, circuit breaker state, and rate limiter available permits.
- How do virtual threads change fan-out handling? Virtual threads make the thread pool sizing less critical for IO, but you still need rate limiters and circuit breakers to protect downstream services.
Visual Overview
Fan-out Amplification
Incoming: 1000 req/s
│
├──▶ Credit Bureau: 1000/s
├──▶ Property API: 1000/s
└──▶ Income Verify: 1000/s
─────────────
Total: 3000/s downstream!
Safeguard Stack per Downstream:
┌─────────────────────────────┐
│ Bulkhead (20 threads max) │
│ Rate Limiter (500/s cap) │
│ Circuit Breaker (50% fail) │
│ Timeout (5 seconds) │
│ Connection Pool (sized) │
└─────────────────────────────┘
Key Takeaway
Fan-out amplification is a critical concern when parallelizing with CompletableFuture. N incoming requests times K downstream calls per request equals N x K downstream load. Without safeguards, you create a self-inflicted DDoS on your own dependencies.
Five safeguards are required per downstream dependency: a bulkhead (bounded thread pool limiting concurrent calls), a rate limiter (capping calls per second), a timeout (preventing thread hang), a circuit breaker (stopping calls when the dependency is failing), and proper connection pool sizing (HikariCP maxPoolSize must handle the amplified concurrent load).
At National Bank of India, every downstream dependency in the loan approval service has its own safeguard stack. The combination of bulkhead, circuit breaker, rate limiter, and timeout is the standard resilience stack for microservices.
Remember one sentence: fan-out multiplies load, so every downstream needs bulkhead, rate limiter, timeout, and circuit breaker.
---