CompletableFuture in Practice: 3 Patterns Every Java Developer Uses
Retry without blocking threads, load data once per key, and pick the right concurrency tool.
CompletableFuture in Practice: 3 Patterns Every Java Developer Uses
Retry without blocking threads, load data once per key, and pick the right concurrency tool.
Most application code does not need complex lock juggling. In day-to-day Spring or microservice work, CompletableFuture shows up in three practical places: retrying a downstream call, loading expensive data without duplicate work, and choosing the right tool instead of synchronized everywhere.
This article covers only what you actually use at the application level.
Pattern 1 — Retry Without Blocking Your Threads
The rule
Retry only transient failures — timeouts, 503, temporary unavailability. Never retry validation errors or 400 responses.
Never use Thread.sleep() inside a service method to wait between retries. It blocks a Tomcat or pool thread that could be serving other requests.
What teams actually use in production
• Spring Retry — @Retryable on service methods
• Resilience4j — Retry + circuit breaker + backoff policies
Both handle exponential backoff without you writing scheduler logic.
If you write it yourself, the idea is simple: run the call async, schedule the next attempt later — do not sleep on the request thread.
Minimal concept (not production-ready — use a library in real apps):
@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 5000, multiplier = 2))
public String callDownstreamApi() {
return restClient.getForObject("/api/data", String.class);
}
That one annotation replaces pages of manual CompletableFuture retry chains. The manual version exists for interviews and libraries — not for your service code.
Pattern 2 — Load Once Per Key
The problem
Fifty threads hit your service at startup. All fifty try to load “countries” from the database at the same time. Without coordination, you get fifty identical queries.
The application-level fix
Store one in-flight CompletableFuture per key in a ConcurrentHashMap. Same key shares one load. Different keys load in parallel.
import java.util.concurrent.*;
public class AsyncDataLoader {
private final ConcurrentHashMap<String, CompletableFuture<String>> inflightLoads =
new ConcurrentHashMap<>();
private final ExecutorService pool = Executors.newFixedThreadPool(4);
public CompletableFuture<String> load(String datasetName) {
return inflightLoads.computeIfAbsent(datasetName, name ->
CompletableFuture.supplyAsync(() -> fetchFromDatabase(name), pool)
.whenComplete((r, ex) -> inflightLoads.remove(name))
);
}
private String fetchFromDatabase(String datasetName) {
return "data-for-" + datasetName;
}
}
What this gives you in practice:
- “countries” and “currencies” load at the same time — different keys, no blocking each other
- Ten threads requesting “countries” share one DB call — computeIfAbsent creates one future
- Use a fixed thread pool for DB/HTTP work — not the default ForkJoinPool

Pattern 3 — Pick the Right Tool (Quick Decision)
At application level, this is the whole decision:
• Retry a downstream call → Spring Retry or Resilience4j (not Thread.sleep loops) • Same expensive load triggered by many threads → ConcurrentHashMap + CompletableFuture with computeIfAbsent • Simple shared counter or HashMap → synchronized or ReentrantLock • Concurrent map reads/writes → ConcurrentHashMap • Multiple named resources with slow creation → ConcurrentHashMap + ReentrantLock per key (double-checked locking)
You rarely need all of these in one class. Pick one pattern per problem.
Summary
Three takeaways for daily coding:
• Retries — use a library; never block service threads with Thread.sleep between attempts • Duplicate loads — computeIfAbsent + CompletableFuture so the same key loads once • Concurrency choice — CompletableFuture for async coordination, ConcurrentHashMap for shared maps, locks only when you need simple exclusive access
Understand the concept. Reach for the library or the 20-line computeIfAbsent pattern. Skip the manual retry state machine unless you are building infrastructure.
메타데이터
- post_id
- 57be76ab54ae
- slug
- completablefuture-in-practice-3-patterns-every-java-developer-uses-57be76ab54ae
- url
- https://medium.com/@pareshyadav.ksp/completablefuture-in-practice-3-patterns-every-java-developer-uses-57be76ab54ae
- canonical_url
- https://medium.com/@pareshyadav.ksp/completablefuture-in-practice-3-patterns-every-java-developer-uses-57be76ab54ae
- author_url
- https://medium.com/@pareshyadav.ksp
- status
- ok
- fetched_at
- 2026-08-10 06:01:41