java 37 lines · 6 steps

Request-scoped rate caching in Spring

A request-scoped bean deduplicates exchange-rate lookups so identical calls within one HTTP request only hit the client once.

Explained by highlit
1@Service
2@RequiredArgsConstructor
3public class ExchangeRateService {
4 
5 private final ExchangeRateClient client;
6 private final RequestScopedRateCache cache;
7 
8 public BigDecimal rateFor(String from, String to) {
9 String key = from + "->" + to;
10 return cache.computeIfAbsent(key, () -> client.fetchRate(from, to));
11 }
12}
13 
14@Component
15@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
16class RequestScopedRateCache {
17 
18 private final ConcurrentMap<String, CompletableFuture<BigDecimal>> inflight = new ConcurrentHashMap<>();
19 
20 BigDecimal computeIfAbsent(String key, Supplier<BigDecimal> loader) {
21 CompletableFuture<BigDecimal> future = inflight.computeIfAbsent(key, k -> {
22 CompletableFuture<BigDecimal> f = new CompletableFuture<>();
23 try {
24 f.complete(loader.get());
25 } catch (RuntimeException ex) {
26 f.completeExceptionally(ex);
27 }
28 return f;
29 });
30 try {
31 return future.join();
32 } catch (CompletionException ex) {
33 inflight.remove(key, future);
34 throw (RuntimeException) ex.getCause();
35 }
36 }
37}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1A request-scoped bean gives you a per-request cache lifetime for free, no manual cleanup between requests.
  2. 2Storing futures rather than values in a ConcurrentMap collapses concurrent identical calls into a single computation.
  3. 3Removing a failed entry keeps failures from being cached, so the next call retries cleanly.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Request-scoped rate caching in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code