← Back to list

CompletableFuture in Practice: 3 Patterns Every Java Developer Uses

Retry without blocking threads, load data once per key, and pick the right concurrency tool.

Paresh Yadav · 2026-07-27 10:09 · 0 claps · 2.3 min read
#java #completablefuture #concurrency #multithreading #concurrenthashmap
Open on Medium ↗
Wiki topics: 📚 · Books & Reading

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