Core Java

Save the Day (and Memory): Java Caching Strategies Using Caffeine and Redis

Caching is one of those things that sounds simple until you actually need it. Then suddenly you’re drowning in cache invalidation strategies, distributed system headaches, and wondering why your application is eating memory like it’s going out of style.

Let’s cut through the noise and look at two heavyweight champions of the Java caching world: Caffeine for local caching and Redis for distributed caching. We’ll cover when to use each, how to implement them properly, and how to avoid the pitfalls that’ll have you debugging at 2 AM.

Why Cache in the First Place?

Before we dive into implementation, let’s be clear about what we’re solving. You cache to:

  • Reduce database load – Stop hammering your database with the same queries
  • Improve response times – Serve data from memory instead of disk or network
  • Handle traffic spikes – Keep your system responsive when things get wild

The trade-off? Memory usage and complexity. Choose your caching strategy wisely.

Caffeine: Your Local Cache Champion

Caffeine is a high-performance, in-memory cache for Java. It’s fast, lightweight, and sits right in your application’s heap. Think of it as your first line of defense.

When to Use Caffeine

Use Caffeine when:

  • You need blazing-fast access (microseconds)
  • Your data is read frequently and doesn’t change often
  • You’re running a single instance or can tolerate cache inconsistency across instances
  • Memory footprint per instance is manageable

Basic Implementation

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

public class UserCache {
    private final Cache<Long, User> cache;
    
    public UserCache() {
        cache = Caffeine.newBuilder()
            .maximumSize(10_000)
            .expireAfterWrite(Duration.ofMinutes(10))
            .recordStats()
            .build();
    }
    
    public User getUser(Long userId) {
        return cache.get(userId, id -> userRepository.findById(id));
    }
}

This cache holds up to 10,000 users, expires entries after 10 minutes, and tracks statistics for monitoring.

Advanced Pattern: Loading Cache with Refresh

LoadingCache<String, ProductList> productCache = Caffeine.newBuilder()
    .maximumSize(1_000)
    .expireAfterWrite(Duration.ofMinutes(30))
    .refreshAfterWrite(Duration.ofMinutes(25))
    .build(key -> productService.fetchProducts(key));

// Usage
ProductList products = productCache.get("electronics");

The refresh mechanism reloads values asynchronously before they expire, preventing the thundering herd problem where many requests wait for cache repopulation.

Redis: Distributed Caching Done Right

Redis takes caching beyond a single machine. It’s your go-to when you need cache consistency across multiple instances or want to share session data across services.

When to Use Redis

Use Redis when:

  • You’re running multiple application instances
  • You need cache consistency across your infrastructure
  • Session management needs to span multiple servers
  • You want advanced data structures (sorted sets, lists, hashes)

Spring Boot + Redis Example

@Configuration
@EnableCaching
public class RedisConfig {
    
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(60))
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
            )
            .disableCachingNullValues();
        
        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .build();
    }
}

@Service
public class OrderService {
    
    @Cacheable(value = "orders", key = "#orderId")
    public Order getOrder(Long orderId) {
        return orderRepository.findById(orderId);
    }
    
    @CacheEvict(value = "orders", key = "#order.id")
    public Order updateOrder(Order order) {
        return orderRepository.save(order);
    }
}

The annotations make caching declarative, but be careful—they hide complexity that can bite you during debugging.

The Two-Tier Strategy: Best of Both Worlds

Why choose when you can have both? A two-tier cache combines Caffeine’s speed with Redis’s distribution.

@Service
public class ProductService {
    private final LoadingCache<String, Product> l1Cache;
    private final RedisTemplate<String, Product> redisTemplate;
    
    public ProductService(RedisTemplate<String, Product> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.l1Cache = Caffeine.newBuilder()
            .maximumSize(5_000)
            .expireAfterWrite(Duration.ofMinutes(5))
            .build(this::fetchFromL2OrDatabase);
    }
    
    public Product getProduct(String productId) {
        return l1Cache.get(productId);
    }
    
    private Product fetchFromL2OrDatabase(String productId) {
        // Try Redis first
        Product product = redisTemplate.opsForValue().get("product:" + productId);
        if (product != null) {
            return product;
        }
        
        // Fall back to database
        product = productRepository.findById(productId);
        if (product != null) {
            redisTemplate.opsForValue().set(
                "product:" + productId, 
                product, 
                Duration.ofMinutes(30)
            );
        }
        return product;
    }
}

This pattern gives you microsecond local access with distributed consistency as a safety net.

Cache Invalidation: The Hard Part

Phil Karlton famously said there are only two hard things in computer science: cache invalidation and naming things. He wasn’t wrong.

Strategies That Actually Work

Time-based expiration: Simple but can serve stale data. Fine for analytics or non-critical reads.

Event-driven invalidation: Use Redis pub/sub or message queues to broadcast updates.

@Service
public class CacheInvalidationService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    public void invalidateProductCache(String productId) {
        // Invalidate Redis
        redisTemplate.delete("product:" + productId);
        
        // Publish invalidation event for other instances
        redisTemplate.convertAndSend("cache-invalidation", productId);
    }
}

Write-through cache: Update cache and database atomically. More complex but maintains consistency.

Monitoring: You Can’t Fix What You Can’t See

Always track your cache performance:

CacheStats stats = cache.stats();
logger.info("Cache stats - Hits: {}, Misses: {}, Hit Rate: {}%", 
    stats.hitCount(), 
    stats.missCount(), 
    stats.hitRate() * 100);

Watch for:

  • Hit rate dropping below 80% (might need larger cache or better eviction policy)
  • High miss rate on startup (consider cache warming)
  • Memory pressure causing evictions

Common Pitfalls to Avoid

  • Over-caching: Not everything needs to be cached. If it’s accessed once per day, skip it.
  • Under-sizing: A cache that constantly evicts hot data is worse than no cache.
  • Ignoring serialization cost: Redis requires serialization. Large objects can kill performance.
  • Forgetting about cache warming: Cold caches on deployment can crush your database.
  • No monitoring: Flying blind leads to production surprises.

Useful Resources

Documentation & Guides

Libraries & Tools

Performance & Best Practices

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button