import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
@Configuration
public class RateProviderConfig {
@Bean
public LoadingCache<CurrencyPair, BigDecimal> rateCache(FxRateClient client) {
CacheLoader<CurrencyPair, BigDecimal> loader = key -> client.fetchRate(key);
return Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.refreshAfterWrite(2, TimeUnit.MINUTES)
.recordStats()
.build(loader);
}
}
import com.github.benmanes.caffeine.cache.LoadingCache;
import com.github.benmanes.caffeine.cache.stats.CacheStats;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.concurrent.CompletionException;
@Service
public class ExchangeRateService {
private final LoadingCache<CurrencyPair, BigDecimal> cache;
public ExchangeRateService(LoadingCache<CurrencyPair, BigDecimal> cache) {
this.cache = cache;
}
public BigDecimal getRate(CurrencyPair pair) {
try {
return cache.get(pair);
} catch (CompletionException ex) {
throw new RateUnavailableException("No rate for " + pair, ex.getCause());
}
}
public BigDecimal convert(CurrencyPair pair, BigDecimal amount) {
return amount.multiply(getRate(pair));
}
public CacheStats stats() {
return cache.stats();
}
}
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import java.math.BigDecimal;
@Component
public class FxRateClient {
private final RestTemplate rest;
private final String baseUrl;
public FxRateClient(RestTemplate rest) {
this.rest = rest;
this.baseUrl = "https://fx.example.com/v1/rate";
}
public BigDecimal fetchRate(CurrencyPair pair) {
String url = baseUrl + "?from={from}&to={to}";
RateResponse body = rest.getForObject(url, RateResponse.class, pair.from(), pair.to());
if (body == null || body.rate() == null) {
throw new IllegalStateException("Empty rate response for " + pair);
}
return new BigDecimal(body.rate());
}
public record RateResponse(String rate, String asOf) {}
}
This snippet shows how an expensive remote lookup — foreign-exchange rates from a rate-limited upstream provider — is fronted by a Caffeine cache with a time-based eviction policy. The pattern applies whenever a value is costly to compute or fetch, changes slowly, and can tolerate being slightly stale in exchange for far fewer upstream calls.
In RateProviderConfig, a LoadingCache<CurrencyPair, BigDecimal> is built declaratively. The key settings are expireAfterWrite(5, MINUTES), which fixes a hard TTL so every entry is refetched at least once per window, and refreshAfterWrite(2, MINUTES), which triggers an asynchronous reload while continuing to serve the old value. That distinction matters: expireAfterWrite blocks the requesting thread when an entry is fully evicted, whereas refreshAfterWrite keeps latency flat by returning the stale value and refreshing in the background. maximumSize caps memory so an unbounded set of currency pairs cannot exhaust the heap, and recordStats() exposes hit/miss metrics for tuning. The CacheLoader delegates to the real fetcher, and build wires the loader in so get transparently populates misses.
ExchangeRateService is what the rest of the application calls. Its getRate method calls cache.get(pair), which returns instantly on a hit and only invokes the upstream client on a miss. Caffeine guarantees the loader runs at most once per key even under concurrent access, so a burst of requests for the same pair collapses into a single upstream call rather than a thundering herd. Any checked exception from the loader surfaces as an unchecked CompletionException, which is unwrapped here into a domain-specific RateUnavailableException. The stats method surfaces the recorded counters for observability.
FxRateClient is the genuinely expensive collaborator — an HTTP call with parsing — kept deliberately unaware of caching so it stays testable in isolation. A subtle pitfall worth noting: refreshAfterWrite reloads on the next access after the interval, not on a timer, so a key that is never read again will still expire via expireAfterWrite rather than being refreshed forever. Choosing the refresh interval shorter than the expiry keeps hot keys warm while cold keys are eventually dropped.
Related snips
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
use crossbeam::channel::unbounded;
use std::thread;
fn main() {
let (tx, rx) = unbounded();
Crossbeam for advanced concurrent data structures
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = false
Bullet.bullet_logger = true
Bullet.console = true
N+1 query detection with Bullet gem
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
Channels (mpsc) for message passing between threads
Share this code
Here's the card — post it anywhere.