Back to Journal
System Design

Complete Guide to Distributed Caching with Java

A comprehensive guide to implementing Distributed Caching using Java, covering architecture, code examples, and production-ready patterns.

Muneer Puthiya Purayil 18 min read

Java's Spring ecosystem provides the most comprehensive distributed caching support of any language through Spring Cache abstraction, Spring Data Redis, and integration with Caffeine for local caching. This guide covers implementing production-ready distributed caching in Java with Spring Boot and Redis.

Spring Cache Configuration

java
1@Configuration
2@EnableCaching
3public class CacheConfig {
4 
5 @Bean
6 public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
7 RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
8 .entryTtl(Duration.ofMinutes(10))
9 .serializeValuesWith(
10 RedisSerializationContext.SerializationPair.fromSerializer(
11 new GenericJackson2JsonRedisSerializer()
12 )
13 )
14 .disableCachingNullValues();
15 
16 Map<String, RedisCacheConfiguration> configs = Map.of(
17 "products", defaults.entryTtl(Duration.ofHours(1)),
18 "users", defaults.entryTtl(Duration.ofMinutes(5)),
19 "sessions", defaults.entryTtl(Duration.ofHours(24))
20 );
21 
22 return RedisCacheManager.builder(factory)
23 .cacheDefaults(defaults)
24 .withInitialCacheConfigurations(configs)
25 .build();
26 }
27 
28 @Bean
29 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
30 RedisTemplate<String, Object> template = new RedisTemplate<>();
31 template.setConnectionFactory(factory);
32 template.setKeySerializer(new StringRedisSerializer());
33 template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
34 template.setHashKeySerializer(new StringRedisSerializer());
35 template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
36 return template;
37 }
38}
39 

Annotation-Based Caching

java
1@Service
2public class ProductService {
3 
4 private final ProductRepository repository;
5 
6 @Cacheable(value = "products", key = "#id")
7 public Product getById(String id) {
8 return repository.findById(id)
9 .orElseThrow(() -> new ProductNotFoundException(id));
10 }
11 
12 @CachePut(value = "products", key = "#result.id")
13 public Product update(String id, ProductUpdate update) {
14 Product product = repository.findById(id)
15 .orElseThrow(() -> new ProductNotFoundException(id));
16 product.apply(update);
17 return repository.save(product);
18 }
19 
20 @CacheEvict(value = "products", key = "#id")
21 public void delete(String id) {
22 repository.deleteById(id);
23 }
24 
25 @CacheEvict(value = "products", allEntries = true)
26 public void clearCache() {
27 // Invoked manually or on schedule
28 }
29}
30 

Multi-Level Caching with Caffeine + Redis

java
1@Configuration
2public class MultiLevelCacheConfig {
3 
4 @Bean
5 public CacheManager cacheManager(RedisConnectionFactory factory) {
6 // L1: Caffeine (in-process, microsecond access)
7 CaffeineCacheManager l1 = new CaffeineCacheManager();
8 l1.setCaffeine(Caffeine.newBuilder()
9 .maximumSize(10_000)
10 .expireAfterWrite(Duration.ofMinutes(1)));
11 
12 // L2: Redis (millisecond access)
13 RedisCacheManager l2 = RedisCacheManager.builder(factory)
14 .cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
15 .entryTtl(Duration.ofMinutes(10)))
16 .build();
17 
18 return new CompositeCacheManager(l1, l2);
19 }
20}
21 

Need a second opinion on your system design architecture?

I run free 30-minute strategy calls for engineering teams tackling this exact problem.

Book a Free Call

Manual Cache Operations

java
1@Service
2public class CacheService {
3 
4 private final RedisTemplate<String, Object> redis;
5 
6 public <T> Optional<T> get(String key, Class<T> type) {
7 Object value = redis.opsForValue().get(key);
8 return Optional.ofNullable(type.cast(value));
9 }
10 
11 public void set(String key, Object value, Duration ttl) {
12 redis.opsForValue().set(key, value, ttl);
13 }
14 
15 public void delete(String... keys) {
16 redis.delete(Arrays.asList(keys));
17 }
18 
19 public <T> T getOrLoad(String key, Class<T> type, Duration ttl, Supplier<T> loader) {
20 return get(key, type).orElseGet(() -> {
21 T value = loader.get();
22 set(key, value, ttl);
23 return value;
24 });
25 }
26 
27 // Batch operations
28 public Map<String, Object> multiGet(Collection<String> keys) {
29 List<Object> values = redis.opsForValue().multiGet(keys);
30 Map<String, Object> result = new HashMap<>();
31 List<String> keyList = new ArrayList<>(keys);
32 for (int i = 0; i < keyList.size(); i++) {
33 if (values.get(i) != null) {
34 result.put(keyList.get(i), values.get(i));
35 }
36 }
37 return result;
38 }
39}
40 

Cache Stampede Protection

java
1@Component
2public class SingleFlightCache {
3 
4 private final RedisTemplate<String, Object> redis;
5 private final ConcurrentHashMap<String, CompletableFuture<?>> inflight = new ConcurrentHashMap<>();
6 
7 @SuppressWarnings("unchecked")
8 public <T> CompletableFuture<T> getOrLoad(
9 String key, Class<T> type, Duration ttl, Supplier<T> loader
10 ) {
11 Object cached = redis.opsForValue().get(key);
12 if (cached != null) {
13 return CompletableFuture.completedFuture(type.cast(cached));
14 }
15 
16 return (CompletableFuture<T>) inflight.computeIfAbsent(key, k ->
17 CompletableFuture.supplyAsync(() -> {
18 // Double-check
19 Object rechecked = redis.opsForValue().get(key);
20 if (rechecked != null) return type.cast(rechecked);
21 
22 T value = loader.get();
23 redis.opsForValue().set(key, value, ttl);
24 return value;
25 }).whenComplete((result, error) -> inflight.remove(key))
26 );
27 }
28}
29 

Monitoring with Micrometer

java
1@Component
2public class CacheMetrics {
3 
4 private final MeterRegistry registry;
5 
6 public CacheMetrics(MeterRegistry registry) {
7 this.registry = registry;
8 }
9 
10 public void recordHit(String cacheName) {
11 registry.counter("cache.hits", "name", cacheName).increment();
12 }
13 
14 public void recordMiss(String cacheName) {
15 registry.counter("cache.misses", "name", cacheName).increment();
16 }
17 
18 public void recordLatency(String cacheName, String operation, Duration latency) {
19 registry.timer("cache.latency", "name", cacheName, "op", operation)
20 .record(latency);
21 }
22}
23 

Conclusion

Java's Spring ecosystem provides the most feature-complete distributed caching integration available. The @Cacheable annotation eliminates boilerplate for common cache-aside patterns, Spring Data Redis handles serialization and connection management, and Caffeine provides a high-performance local cache for L1 caching. For teams already using Spring Boot, adding distributed caching requires minimal new code and integrates seamlessly with existing monitoring through Micrometer.

FAQ

Need expert help?

Building with system design?

I help teams ship production-grade systems. From architecture review to hands-on builds.

Muneer Puthiya Purayil

SaaS Architect & AI Systems Engineer. 10+ years shipping production infrastructure across fintech, automotive, e-commerce, and healthcare.

Engage

Start a
Conversation.

For teams building at scale: SaaS platforms, agentic AI systems, and enterprise mobile infrastructure. Scope and fit are evaluated before any engagement begins.

Limited availability · Q3 / Q4 2026