As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!
Java lightweight caching is a vital technique for enhancing application performance. By storing frequently accessed data in memory, caching reduces database queries and computation overhead, resulting in faster response times and reduced resource consumption. I’ve implemented various caching solutions across multiple projects and found several strategies particularly effective.
Caching with Caffeine
Caffeine is my preferred Java caching library for high-performance applications. It offers exceptional speed with minimal overhead and provides intelligent features like automatic expiration and size-based eviction.
The library uses an adaptive algorithm that combines frequency and recency to determine which entries to keep, making it more effective than simple LRU (Least Recently Used) implementations.
Implementation is straightforward:
<span>import</span> <span>com.github.benmanes.caffeine.cache.Caffeine</span><span>;</span><span>import</span> <span>com.github.benmanes.caffeine.cache.Cache</span><span>;</span><span>import</span> <span>com.github.benmanes.caffeine.cache.LoadingCache</span><span>;</span><span>import</span> <span>java.time.Duration</span><span>;</span><span>import</span> <span>java.util.concurrent.TimeUnit</span><span>;</span><span>public</span> <span>class</span> <span>CaffeineExample</span> <span>{</span><span>public</span> <span>void</span> <span>simpleCache</span><span>()</span> <span>{</span><span>// Manual cache population</span><span>Cache</span><span><</span><span>String</span><span>,</span> <span>User</span><span>></span> <span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>5</span><span>))</span><span>.</span><span>recordStats</span><span>()</span> <span>// Optional for monitoring</span><span>.</span><span>build</span><span>();</span><span>// Get a value, providing a function to calculate it if not found</span><span>User</span> <span>user</span> <span>=</span> <span>cache</span><span>.</span><span>get</span><span>(</span><span>"user123"</span><span>,</span> <span>key</span> <span>-></span> <span>fetchUserFromDatabase</span><span>(</span><span>key</span><span>));</span><span>// Or explicitly manage values</span><span>cache</span><span>.</span><span>put</span><span>(</span><span>"user456"</span><span>,</span> <span>new</span> <span>User</span><span>(</span><span>"John Doe"</span><span>));</span><span>User</span> <span>cachedUser</span> <span>=</span> <span>cache</span><span>.</span><span>getIfPresent</span><span>(</span><span>"user456"</span><span>);</span><span>// Invalidate when needed</span><span>cache</span><span>.</span><span>invalidate</span><span>(</span><span>"user456"</span><span>);</span><span>}</span><span>public</span> <span>void</span> <span>loadingCache</span><span>()</span> <span>{</span><span>// Automatic cache population</span><span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>User</span><span>></span> <span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>5</span><span>,</span> <span>TimeUnit</span><span>.</span><span>MINUTES</span><span>)</span><span>.</span><span>refreshAfterWrite</span><span>(</span><span>1</span><span>,</span> <span>TimeUnit</span><span>.</span><span>MINUTES</span><span>)</span> <span>// Async refresh</span><span>.</span><span>build</span><span>(</span><span>this</span><span>::</span><span>fetchUserFromDatabase</span><span>);</span><span>// Values are loaded automatically if not present</span><span>User</span> <span>user</span> <span>=</span> <span>cache</span><span>.</span><span>get</span><span>(</span><span>"user123"</span><span>);</span><span>// Batch operations also available</span><span>Map</span><span><</span><span>String</span><span>,</span> <span>User</span><span>></span> <span>users</span> <span>=</span> <span>cache</span><span>.</span><span>getAll</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"user1"</span><span>,</span> <span>"user2"</span><span>));</span><span>}</span><span>private</span> <span>User</span> <span>fetchUserFromDatabase</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span><span>// Database call logic here</span><span>return</span> <span>new</span> <span>User</span><span>(</span><span>userId</span><span>);</span><span>}</span><span>}</span><span>import</span> <span>com.github.benmanes.caffeine.cache.Caffeine</span><span>;</span> <span>import</span> <span>com.github.benmanes.caffeine.cache.Cache</span><span>;</span> <span>import</span> <span>com.github.benmanes.caffeine.cache.LoadingCache</span><span>;</span> <span>import</span> <span>java.time.Duration</span><span>;</span> <span>import</span> <span>java.util.concurrent.TimeUnit</span><span>;</span> <span>public</span> <span>class</span> <span>CaffeineExample</span> <span>{</span> <span>public</span> <span>void</span> <span>simpleCache</span><span>()</span> <span>{</span> <span>// Manual cache population</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>User</span><span>></span> <span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>5</span><span>))</span> <span>.</span><span>recordStats</span><span>()</span> <span>// Optional for monitoring</span> <span>.</span><span>build</span><span>();</span> <span>// Get a value, providing a function to calculate it if not found</span> <span>User</span> <span>user</span> <span>=</span> <span>cache</span><span>.</span><span>get</span><span>(</span><span>"user123"</span><span>,</span> <span>key</span> <span>-></span> <span>fetchUserFromDatabase</span><span>(</span><span>key</span><span>));</span> <span>// Or explicitly manage values</span> <span>cache</span><span>.</span><span>put</span><span>(</span><span>"user456"</span><span>,</span> <span>new</span> <span>User</span><span>(</span><span>"John Doe"</span><span>));</span> <span>User</span> <span>cachedUser</span> <span>=</span> <span>cache</span><span>.</span><span>getIfPresent</span><span>(</span><span>"user456"</span><span>);</span> <span>// Invalidate when needed</span> <span>cache</span><span>.</span><span>invalidate</span><span>(</span><span>"user456"</span><span>);</span> <span>}</span> <span>public</span> <span>void</span> <span>loadingCache</span><span>()</span> <span>{</span> <span>// Automatic cache population</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>User</span><span>></span> <span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>5</span><span>,</span> <span>TimeUnit</span><span>.</span><span>MINUTES</span><span>)</span> <span>.</span><span>refreshAfterWrite</span><span>(</span><span>1</span><span>,</span> <span>TimeUnit</span><span>.</span><span>MINUTES</span><span>)</span> <span>// Async refresh</span> <span>.</span><span>build</span><span>(</span><span>this</span><span>::</span><span>fetchUserFromDatabase</span><span>);</span> <span>// Values are loaded automatically if not present</span> <span>User</span> <span>user</span> <span>=</span> <span>cache</span><span>.</span><span>get</span><span>(</span><span>"user123"</span><span>);</span> <span>// Batch operations also available</span> <span>Map</span><span><</span><span>String</span><span>,</span> <span>User</span><span>></span> <span>users</span> <span>=</span> <span>cache</span><span>.</span><span>getAll</span><span>(</span><span>Arrays</span><span>.</span><span>asList</span><span>(</span><span>"user1"</span><span>,</span> <span>"user2"</span><span>));</span> <span>}</span> <span>private</span> <span>User</span> <span>fetchUserFromDatabase</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span> <span>// Database call logic here</span> <span>return</span> <span>new</span> <span>User</span><span>(</span><span>userId</span><span>);</span> <span>}</span> <span>}</span>import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.LoadingCache; import java.time.Duration; import java.util.concurrent.TimeUnit; public class CaffeineExample { public void simpleCache() { // Manual cache population Cache<String, User> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(5)) .recordStats() // Optional for monitoring .build(); // Get a value, providing a function to calculate it if not found User user = cache.get("user123", key -> fetchUserFromDatabase(key)); // Or explicitly manage values cache.put("user456", new User("John Doe")); User cachedUser = cache.getIfPresent("user456"); // Invalidate when needed cache.invalidate("user456"); } public void loadingCache() { // Automatic cache population LoadingCache<String, User> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(5, TimeUnit.MINUTES) .refreshAfterWrite(1, TimeUnit.MINUTES) // Async refresh .build(this::fetchUserFromDatabase); // Values are loaded automatically if not present User user = cache.get("user123"); // Batch operations also available Map<String, User> users = cache.getAll(Arrays.asList("user1", "user2")); } private User fetchUserFromDatabase(String userId) { // Database call logic here return new User(userId); } }
Enter fullscreen mode Exit fullscreen mode
When implementing Caffeine, I consider these key parameters:
-
maximumSize
: Limits memory usage by evicting less valuable entries -
expireAfterWrite
: Removes entries after a set time from creation -
expireAfterAccess
: Removes entries after a period without access -
refreshAfterWrite
: Updates entries asynchronously while returning stale values
The performance difference between Caffeine and older libraries like Guava Cache is substantial, particularly under high concurrency.
Distributed Caching with Redis
When working with clustered or microservice applications, I’ve found Redis invaluable for sharing cached data across multiple instances. Redis functions as a central, in-memory data store while providing persistence options.
Here’s how I implement Redis caching in Java applications:
<span>import</span> <span>org.springframework.data.redis.core.RedisTemplate</span><span>;</span><span>import</span> <span>org.springframework.data.redis.core.ValueOperations</span><span>;</span><span>import</span> <span>org.springframework.stereotype.Service</span><span>;</span><span>import</span> <span>java.time.Duration</span><span>;</span><span>@Service</span><span>public</span> <span>class</span> <span>RedisCacheService</span> <span>{</span><span>private</span> <span>final</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>;</span><span>private</span> <span>final</span> <span>ValueOperations</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>valueOps</span><span>;</span><span>public</span> <span>RedisCacheService</span><span>(</span><span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>redisTemplate</span> <span>=</span> <span>redisTemplate</span><span>;</span><span>this</span><span>.</span><span>valueOps</span> <span>=</span> <span>redisTemplate</span><span>.</span><span>opsForValue</span><span>();</span><span>}</span><span>public</span> <span>void</span> <span>cacheData</span><span>(</span><span>String</span> <span>key</span><span>,</span> <span>Object</span> <span>value</span><span>,</span> <span>Duration</span> <span>expiration</span><span>)</span> <span>{</span><span>valueOps</span><span>.</span><span>set</span><span>(</span><span>key</span><span>,</span> <span>value</span><span>,</span> <span>expiration</span><span>);</span><span>}</span><span>public</span> <span>Object</span> <span>getCachedData</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span><span>return</span> <span>valueOps</span><span>.</span><span>get</span><span>(</span><span>key</span><span>);</span><span>}</span><span>public</span> <span>void</span> <span>invalidate</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span><span>redisTemplate</span><span>.</span><span>delete</span><span>(</span><span>key</span><span>);</span><span>}</span><span>// Example of using more complex Redis data structures</span><span>public</span> <span>void</span> <span>incrementCounter</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span><span>valueOps</span><span>.</span><span>increment</span><span>(</span><span>key</span><span>);</span><span>}</span><span>public</span> <span>void</span> <span>addToSet</span><span>(</span><span>String</span> <span>key</span><span>,</span> <span>Object</span><span>...</span> <span>values</span><span>)</span> <span>{</span><span>redisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>add</span><span>(</span><span>key</span><span>,</span> <span>values</span><span>);</span><span>}</span><span>public</span> <span>Set</span><span><</span><span>Object</span><span>></span> <span>getSetMembers</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span><span>return</span> <span>redisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>members</span><span>(</span><span>key</span><span>);</span><span>}</span><span>}</span><span>import</span> <span>org.springframework.data.redis.core.RedisTemplate</span><span>;</span> <span>import</span> <span>org.springframework.data.redis.core.ValueOperations</span><span>;</span> <span>import</span> <span>org.springframework.stereotype.Service</span><span>;</span> <span>import</span> <span>java.time.Duration</span><span>;</span> <span>@Service</span> <span>public</span> <span>class</span> <span>RedisCacheService</span> <span>{</span> <span>private</span> <span>final</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>;</span> <span>private</span> <span>final</span> <span>ValueOperations</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>valueOps</span><span>;</span> <span>public</span> <span>RedisCacheService</span><span>(</span><span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>redisTemplate</span> <span>=</span> <span>redisTemplate</span><span>;</span> <span>this</span><span>.</span><span>valueOps</span> <span>=</span> <span>redisTemplate</span><span>.</span><span>opsForValue</span><span>();</span> <span>}</span> <span>public</span> <span>void</span> <span>cacheData</span><span>(</span><span>String</span> <span>key</span><span>,</span> <span>Object</span> <span>value</span><span>,</span> <span>Duration</span> <span>expiration</span><span>)</span> <span>{</span> <span>valueOps</span><span>.</span><span>set</span><span>(</span><span>key</span><span>,</span> <span>value</span><span>,</span> <span>expiration</span><span>);</span> <span>}</span> <span>public</span> <span>Object</span> <span>getCachedData</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span> <span>return</span> <span>valueOps</span><span>.</span><span>get</span><span>(</span><span>key</span><span>);</span> <span>}</span> <span>public</span> <span>void</span> <span>invalidate</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span> <span>redisTemplate</span><span>.</span><span>delete</span><span>(</span><span>key</span><span>);</span> <span>}</span> <span>// Example of using more complex Redis data structures</span> <span>public</span> <span>void</span> <span>incrementCounter</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span> <span>valueOps</span><span>.</span><span>increment</span><span>(</span><span>key</span><span>);</span> <span>}</span> <span>public</span> <span>void</span> <span>addToSet</span><span>(</span><span>String</span> <span>key</span><span>,</span> <span>Object</span><span>...</span> <span>values</span><span>)</span> <span>{</span> <span>redisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>add</span><span>(</span><span>key</span><span>,</span> <span>values</span><span>);</span> <span>}</span> <span>public</span> <span>Set</span><span><</span><span>Object</span><span>></span> <span>getSetMembers</span><span>(</span><span>String</span> <span>key</span><span>)</span> <span>{</span> <span>return</span> <span>redisTemplate</span><span>.</span><span>opsForSet</span><span>().</span><span>members</span><span>(</span><span>key</span><span>);</span> <span>}</span> <span>}</span>import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.stereotype.Service; import java.time.Duration; @Service public class RedisCacheService { private final RedisTemplate<String, Object> redisTemplate; private final ValueOperations<String, Object> valueOps; public RedisCacheService(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; this.valueOps = redisTemplate.opsForValue(); } public void cacheData(String key, Object value, Duration expiration) { valueOps.set(key, value, expiration); } public Object getCachedData(String key) { return valueOps.get(key); } public void invalidate(String key) { redisTemplate.delete(key); } // Example of using more complex Redis data structures public void incrementCounter(String key) { valueOps.increment(key); } public void addToSet(String key, Object... values) { redisTemplate.opsForSet().add(key, values); } public Set<Object> getSetMembers(String key) { return redisTemplate.opsForSet().members(key); } }
Enter fullscreen mode Exit fullscreen mode
For Spring applications, configuration is simple:
<span>@Configuration</span><span>@EnableRedisRepositories</span><span>public</span> <span>class</span> <span>RedisConfig</span> <span>{</span><span>@Bean</span><span>public</span> <span>RedisConnectionFactory</span> <span>redisConnectionFactory</span><span>()</span> <span>{</span><span>LettuceConnectionFactory</span> <span>factory</span> <span>=</span> <span>new</span> <span>LettuceConnectionFactory</span><span>();</span><span>// Configure connection details if needed</span><span>return</span> <span>factory</span><span>;</span><span>}</span><span>@Bean</span><span>public</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>()</span> <span>{</span><span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>template</span> <span>=</span> <span>new</span> <span>RedisTemplate</span><span><>();</span><span>template</span><span>.</span><span>setConnectionFactory</span><span>(</span><span>redisConnectionFactory</span><span>());</span><span>// Use JSON serialization for values</span><span>Jackson2JsonRedisSerializer</span><span><</span><span>Object</span><span>></span> <span>serializer</span> <span>=</span><span>new</span> <span>Jackson2JsonRedisSerializer</span><span><>(</span><span>Object</span><span>.</span><span>class</span><span>);</span><span>template</span><span>.</span><span>setValueSerializer</span><span>(</span><span>serializer</span><span>);</span><span>template</span><span>.</span><span>setHashValueSerializer</span><span>(</span><span>serializer</span><span>);</span><span>// Use String serialization for keys</span><span>template</span><span>.</span><span>setKeySerializer</span><span>(</span><span>new</span> <span>StringRedisSerializer</span><span>());</span><span>template</span><span>.</span><span>setHashKeySerializer</span><span>(</span><span>new</span> <span>StringRedisSerializer</span><span>());</span><span>template</span><span>.</span><span>afterPropertiesSet</span><span>();</span><span>return</span> <span>template</span><span>;</span><span>}</span><span>}</span><span>@Configuration</span> <span>@EnableRedisRepositories</span> <span>public</span> <span>class</span> <span>RedisConfig</span> <span>{</span> <span>@Bean</span> <span>public</span> <span>RedisConnectionFactory</span> <span>redisConnectionFactory</span><span>()</span> <span>{</span> <span>LettuceConnectionFactory</span> <span>factory</span> <span>=</span> <span>new</span> <span>LettuceConnectionFactory</span><span>();</span> <span>// Configure connection details if needed</span> <span>return</span> <span>factory</span><span>;</span> <span>}</span> <span>@Bean</span> <span>public</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>()</span> <span>{</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>template</span> <span>=</span> <span>new</span> <span>RedisTemplate</span><span><>();</span> <span>template</span><span>.</span><span>setConnectionFactory</span><span>(</span><span>redisConnectionFactory</span><span>());</span> <span>// Use JSON serialization for values</span> <span>Jackson2JsonRedisSerializer</span><span><</span><span>Object</span><span>></span> <span>serializer</span> <span>=</span> <span>new</span> <span>Jackson2JsonRedisSerializer</span><span><>(</span><span>Object</span><span>.</span><span>class</span><span>);</span> <span>template</span><span>.</span><span>setValueSerializer</span><span>(</span><span>serializer</span><span>);</span> <span>template</span><span>.</span><span>setHashValueSerializer</span><span>(</span><span>serializer</span><span>);</span> <span>// Use String serialization for keys</span> <span>template</span><span>.</span><span>setKeySerializer</span><span>(</span><span>new</span> <span>StringRedisSerializer</span><span>());</span> <span>template</span><span>.</span><span>setHashKeySerializer</span><span>(</span><span>new</span> <span>StringRedisSerializer</span><span>());</span> <span>template</span><span>.</span><span>afterPropertiesSet</span><span>();</span> <span>return</span> <span>template</span><span>;</span> <span>}</span> <span>}</span>@Configuration @EnableRedisRepositories public class RedisConfig { @Bean public RedisConnectionFactory redisConnectionFactory() { LettuceConnectionFactory factory = new LettuceConnectionFactory(); // Configure connection details if needed return factory; } @Bean public RedisTemplate<String, Object> redisTemplate() { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory()); // Use JSON serialization for values Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); // Use String serialization for keys template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.afterPropertiesSet(); return template; } }
Enter fullscreen mode Exit fullscreen mode
I’ve seen significant benefits with Redis beyond basic caching:
- Data structures like sets, sorted sets, and lists enable complex operations
- Pub/Sub messaging facilitates cache invalidation across instances
- Cluster mode provides high availability and horizontal scaling
- Redis Streams supports event processing and aggregation
When implementing Redis caching, I handle serialization carefully since all data must be serialized for network transmission. For complex objects, I prefer JSON serialization or purpose-built serializers rather than Java’s default serialization.
Attribute-Level Caching
One strategy I’ve found effective is caching at the attribute level rather than caching entire objects. This approach is particularly valuable for objects with:
- Large size but partially accessed fields
- Expensive computed properties
- Fields with different update frequencies
Here’s an example implementation using Caffeine:
<span>public</span> <span>class</span> <span>UserService</span> <span>{</span><span>private</span> <span>final</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>String</span><span>></span> <span>emailCache</span><span>;</span><span>private</span> <span>final</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>UserProfile</span><span>></span> <span>profileCache</span><span>;</span><span>private</span> <span>final</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>List</span><span><</span><span>Order</span><span>>></span> <span>orderCache</span><span>;</span><span>public</span> <span>UserService</span><span>(</span><span>UserRepository</span> <span>repository</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>emailCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>100_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofHours</span><span>(</span><span>24</span><span>))</span><span>.</span><span>build</span><span>(</span><span>repository:</span><span>:</span><span>findEmailById</span><span>);</span><span>this</span><span>.</span><span>profileCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>30</span><span>))</span><span>.</span><span>build</span><span>(</span><span>repository:</span><span>:</span><span>findProfileById</span><span>);</span><span>this</span><span>.</span><span>orderCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>5_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>5</span><span>))</span><span>.</span><span>build</span><span>(</span><span>repository:</span><span>:</span><span>findRecentOrdersById</span><span>);</span><span>}</span><span>public</span> <span>String</span> <span>getUserEmail</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span><span>return</span> <span>emailCache</span><span>.</span><span>get</span><span>(</span><span>userId</span><span>);</span><span>}</span><span>public</span> <span>UserProfile</span> <span>getUserProfile</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span><span>return</span> <span>profileCache</span><span>.</span><span>get</span><span>(</span><span>userId</span><span>);</span><span>}</span><span>public</span> <span>List</span><span><</span><span>Order</span><span>></span> <span>getRecentOrders</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span><span>return</span> <span>orderCache</span><span>.</span><span>get</span><span>(</span><span>userId</span><span>);</span><span>}</span><span>// When updating a specific attribute, only invalidate relevant cache</span><span>public</span> <span>void</span> <span>updateEmail</span><span>(</span><span>String</span> <span>userId</span><span>,</span> <span>String</span> <span>newEmail</span><span>)</span> <span>{</span><span>userRepository</span><span>.</span><span>updateEmail</span><span>(</span><span>userId</span><span>,</span> <span>newEmail</span><span>);</span><span>emailCache</span><span>.</span><span>invalidate</span><span>(</span><span>userId</span><span>);</span><span>// No need to invalidate other caches</span><span>}</span><span>}</span><span>public</span> <span>class</span> <span>UserService</span> <span>{</span> <span>private</span> <span>final</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>String</span><span>></span> <span>emailCache</span><span>;</span> <span>private</span> <span>final</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>UserProfile</span><span>></span> <span>profileCache</span><span>;</span> <span>private</span> <span>final</span> <span>LoadingCache</span><span><</span><span>String</span><span>,</span> <span>List</span><span><</span><span>Order</span><span>>></span> <span>orderCache</span><span>;</span> <span>public</span> <span>UserService</span><span>(</span><span>UserRepository</span> <span>repository</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>emailCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>100_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofHours</span><span>(</span><span>24</span><span>))</span> <span>.</span><span>build</span><span>(</span><span>repository:</span><span>:</span><span>findEmailById</span><span>);</span> <span>this</span><span>.</span><span>profileCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>30</span><span>))</span> <span>.</span><span>build</span><span>(</span><span>repository:</span><span>:</span><span>findProfileById</span><span>);</span> <span>this</span><span>.</span><span>orderCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>5_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>5</span><span>))</span> <span>.</span><span>build</span><span>(</span><span>repository:</span><span>:</span><span>findRecentOrdersById</span><span>);</span> <span>}</span> <span>public</span> <span>String</span> <span>getUserEmail</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span> <span>return</span> <span>emailCache</span><span>.</span><span>get</span><span>(</span><span>userId</span><span>);</span> <span>}</span> <span>public</span> <span>UserProfile</span> <span>getUserProfile</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span> <span>return</span> <span>profileCache</span><span>.</span><span>get</span><span>(</span><span>userId</span><span>);</span> <span>}</span> <span>public</span> <span>List</span><span><</span><span>Order</span><span>></span> <span>getRecentOrders</span><span>(</span><span>String</span> <span>userId</span><span>)</span> <span>{</span> <span>return</span> <span>orderCache</span><span>.</span><span>get</span><span>(</span><span>userId</span><span>);</span> <span>}</span> <span>// When updating a specific attribute, only invalidate relevant cache</span> <span>public</span> <span>void</span> <span>updateEmail</span><span>(</span><span>String</span> <span>userId</span><span>,</span> <span>String</span> <span>newEmail</span><span>)</span> <span>{</span> <span>userRepository</span><span>.</span><span>updateEmail</span><span>(</span><span>userId</span><span>,</span> <span>newEmail</span><span>);</span> <span>emailCache</span><span>.</span><span>invalidate</span><span>(</span><span>userId</span><span>);</span> <span>// No need to invalidate other caches</span> <span>}</span> <span>}</span>public class UserService { private final LoadingCache<String, String> emailCache; private final LoadingCache<String, UserProfile> profileCache; private final LoadingCache<String, List<Order>> orderCache; public UserService(UserRepository repository) { this.emailCache = Caffeine.newBuilder() .maximumSize(100_000) .expireAfterWrite(Duration.ofHours(24)) .build(repository::findEmailById); this.profileCache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(30)) .build(repository::findProfileById); this.orderCache = Caffeine.newBuilder() .maximumSize(5_000) .expireAfterWrite(Duration.ofMinutes(5)) .build(repository::findRecentOrdersById); } public String getUserEmail(String userId) { return emailCache.get(userId); } public UserProfile getUserProfile(String userId) { return profileCache.get(userId); } public List<Order> getRecentOrders(String userId) { return orderCache.get(userId); } // When updating a specific attribute, only invalidate relevant cache public void updateEmail(String userId, String newEmail) { userRepository.updateEmail(userId, newEmail); emailCache.invalidate(userId); // No need to invalidate other caches } }
Enter fullscreen mode Exit fullscreen mode
This approach provides several advantages:
- Reduced memory usage by caching only what’s needed
- Different expiration policies for different attributes
- More precise cache invalidation when data changes
- Higher cache hit rates for frequently accessed attributes
I’ve had success implementing this pattern in user profile services where basic information rarely changes but activity data updates frequently.
Layered Caching Strategies
In high-performance systems, I often implement multiple cache layers with different characteristics. This approach combines the speed of local caches with the sharing capabilities of distributed caches.
A typical implementation includes:
<span>public</span> <span>class</span> <span>LayeredCacheService</span> <span>{</span><span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Product</span><span>></span> <span>localCache</span><span>;</span><span>private</span> <span>final</span> <span>RedisCacheService</span> <span>distributedCache</span><span>;</span><span>private</span> <span>final</span> <span>ProductRepository</span> <span>repository</span><span>;</span><span>public</span> <span>LayeredCacheService</span><span>(</span><span>RedisCacheService</span> <span>distributedCache</span><span>,</span><span>ProductRepository</span> <span>repository</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>localCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>1_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>5</span><span>))</span><span>.</span><span>build</span><span>();</span><span>this</span><span>.</span><span>distributedCache</span> <span>=</span> <span>distributedCache</span><span>;</span><span>this</span><span>.</span><span>repository</span> <span>=</span> <span>repository</span><span>;</span><span>}</span><span>public</span> <span>Product</span> <span>getProduct</span><span>(</span><span>String</span> <span>productId</span><span>)</span> <span>{</span><span>// First check local cache</span><span>Product</span> <span>product</span> <span>=</span> <span>localCache</span><span>.</span><span>getIfPresent</span><span>(</span><span>productId</span><span>);</span><span>if</span> <span>(</span><span>product</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span><span>return</span> <span>product</span><span>;</span><span>}</span><span>// Then check distributed cache</span><span>product</span> <span>=</span> <span>(</span><span>Product</span><span>)</span> <span>distributedCache</span><span>.</span><span>getCachedData</span><span>(</span><span>"product:"</span> <span>+</span> <span>productId</span><span>);</span><span>if</span> <span>(</span><span>product</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span><span>// Populate local cache with result from distributed cache</span><span>localCache</span><span>.</span><span>put</span><span>(</span><span>productId</span><span>,</span> <span>product</span><span>);</span><span>return</span> <span>product</span><span>;</span><span>}</span><span>// If not found in any cache, fetch from database</span><span>product</span> <span>=</span> <span>repository</span><span>.</span><span>findById</span><span>(</span><span>productId</span><span>)</span><span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>ProductNotFoundException</span><span>(</span><span>productId</span><span>));</span><span>// Populate both caches</span><span>localCache</span><span>.</span><span>put</span><span>(</span><span>productId</span><span>,</span> <span>product</span><span>);</span><span>distributedCache</span><span>.</span><span>cacheData</span><span>(</span><span>"product:"</span> <span>+</span> <span>productId</span><span>,</span> <span>product</span><span>,</span> <span>Duration</span><span>.</span><span>ofHours</span><span>(</span><span>1</span><span>));</span><span>return</span> <span>product</span><span>;</span><span>}</span><span>public</span> <span>void</span> <span>invalidateProduct</span><span>(</span><span>String</span> <span>productId</span><span>)</span> <span>{</span><span>// Invalidate in both caches</span><span>localCache</span><span>.</span><span>invalidate</span><span>(</span><span>productId</span><span>);</span><span>distributedCache</span><span>.</span><span>invalidate</span><span>(</span><span>"product:"</span> <span>+</span> <span>productId</span><span>);</span><span>}</span><span>}</span><span>public</span> <span>class</span> <span>LayeredCacheService</span> <span>{</span> <span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Product</span><span>></span> <span>localCache</span><span>;</span> <span>private</span> <span>final</span> <span>RedisCacheService</span> <span>distributedCache</span><span>;</span> <span>private</span> <span>final</span> <span>ProductRepository</span> <span>repository</span><span>;</span> <span>public</span> <span>LayeredCacheService</span><span>(</span><span>RedisCacheService</span> <span>distributedCache</span><span>,</span> <span>ProductRepository</span> <span>repository</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>localCache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>1_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>5</span><span>))</span> <span>.</span><span>build</span><span>();</span> <span>this</span><span>.</span><span>distributedCache</span> <span>=</span> <span>distributedCache</span><span>;</span> <span>this</span><span>.</span><span>repository</span> <span>=</span> <span>repository</span><span>;</span> <span>}</span> <span>public</span> <span>Product</span> <span>getProduct</span><span>(</span><span>String</span> <span>productId</span><span>)</span> <span>{</span> <span>// First check local cache</span> <span>Product</span> <span>product</span> <span>=</span> <span>localCache</span><span>.</span><span>getIfPresent</span><span>(</span><span>productId</span><span>);</span> <span>if</span> <span>(</span><span>product</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span> <span>return</span> <span>product</span><span>;</span> <span>}</span> <span>// Then check distributed cache</span> <span>product</span> <span>=</span> <span>(</span><span>Product</span><span>)</span> <span>distributedCache</span><span>.</span><span>getCachedData</span><span>(</span><span>"product:"</span> <span>+</span> <span>productId</span><span>);</span> <span>if</span> <span>(</span><span>product</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span> <span>// Populate local cache with result from distributed cache</span> <span>localCache</span><span>.</span><span>put</span><span>(</span><span>productId</span><span>,</span> <span>product</span><span>);</span> <span>return</span> <span>product</span><span>;</span> <span>}</span> <span>// If not found in any cache, fetch from database</span> <span>product</span> <span>=</span> <span>repository</span><span>.</span><span>findById</span><span>(</span><span>productId</span><span>)</span> <span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>ProductNotFoundException</span><span>(</span><span>productId</span><span>));</span> <span>// Populate both caches</span> <span>localCache</span><span>.</span><span>put</span><span>(</span><span>productId</span><span>,</span> <span>product</span><span>);</span> <span>distributedCache</span><span>.</span><span>cacheData</span><span>(</span><span>"product:"</span> <span>+</span> <span>productId</span><span>,</span> <span>product</span><span>,</span> <span>Duration</span><span>.</span><span>ofHours</span><span>(</span><span>1</span><span>));</span> <span>return</span> <span>product</span><span>;</span> <span>}</span> <span>public</span> <span>void</span> <span>invalidateProduct</span><span>(</span><span>String</span> <span>productId</span><span>)</span> <span>{</span> <span>// Invalidate in both caches</span> <span>localCache</span><span>.</span><span>invalidate</span><span>(</span><span>productId</span><span>);</span> <span>distributedCache</span><span>.</span><span>invalidate</span><span>(</span><span>"product:"</span> <span>+</span> <span>productId</span><span>);</span> <span>}</span> <span>}</span>public class LayeredCacheService { private final Cache<String, Product> localCache; private final RedisCacheService distributedCache; private final ProductRepository repository; public LayeredCacheService(RedisCacheService distributedCache, ProductRepository repository) { this.localCache = Caffeine.newBuilder() .maximumSize(1_000) .expireAfterWrite(Duration.ofMinutes(5)) .build(); this.distributedCache = distributedCache; this.repository = repository; } public Product getProduct(String productId) { // First check local cache Product product = localCache.getIfPresent(productId); if (product != null) { return product; } // Then check distributed cache product = (Product) distributedCache.getCachedData("product:" + productId); if (product != null) { // Populate local cache with result from distributed cache localCache.put(productId, product); return product; } // If not found in any cache, fetch from database product = repository.findById(productId) .orElseThrow(() -> new ProductNotFoundException(productId)); // Populate both caches localCache.put(productId, product); distributedCache.cacheData("product:" + productId, product, Duration.ofHours(1)); return product; } public void invalidateProduct(String productId) { // Invalidate in both caches localCache.invalidate(productId); distributedCache.invalidate("product:" + productId); } }
Enter fullscreen mode Exit fullscreen mode
This two-tier approach combines the benefits of both worlds:
- Local cache provides sub-millisecond access for repeated requests from the same instance
- Distributed cache ensures consistency across multiple application instances
- Database is shielded from excessive load
- Different expiration policies can be applied at each level
For more complex systems, I’ve implemented three-tier caches adding a near-cache layer with TTL-based automatic refresh.
Optimistic Caching with Cache-Aside Pattern
The cache-aside pattern places caching logic in application code rather than using a transparent caching mechanism. I’ve found this approach provides better control over caching behavior and is more resilient to failures.
Here’s an implementation I use frequently:
<span>@Service</span><span>public</span> <span>class</span> <span>CacheAsideService</span> <span>{</span><span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Optional</span><span><</span><span>Customer</span><span>>></span> <span>cache</span><span>;</span><span>private</span> <span>final</span> <span>CustomerRepository</span> <span>repository</span><span>;</span><span>public</span> <span>CacheAsideService</span><span>(</span><span>CustomerRepository</span> <span>repository</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>repository</span> <span>=</span> <span>repository</span><span>;</span><span>this</span><span>.</span><span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>15</span><span>))</span><span>.</span><span>build</span><span>();</span><span>}</span><span>public</span> <span>Customer</span> <span>getCustomer</span><span>(</span><span>String</span> <span>customerId</span><span>)</span> <span>{</span><span>// Retrieve from cache, including negative caching with Optional</span><span>Optional</span><span><</span><span>Customer</span><span>></span> <span>cachedResult</span> <span>=</span> <span>cache</span><span>.</span><span>getIfPresent</span><span>(</span><span>customerId</span><span>);</span><span>if</span> <span>(</span><span>cachedResult</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span><span>// Return cached result or throw exception for negative cache hit</span><span>return</span> <span>cachedResult</span><span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span><span>new</span> <span>CustomerNotFoundException</span><span>(</span><span>customerId</span><span>));</span><span>}</span><span>try</span> <span>{</span><span>// Cache miss - retrieve from database</span><span>Customer</span> <span>customer</span> <span>=</span> <span>repository</span><span>.</span><span>findById</span><span>(</span><span>customerId</span><span>)</span><span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>CustomerNotFoundException</span><span>(</span><span>customerId</span><span>));</span><span>// Store positive result in cache</span><span>cache</span><span>.</span><span>put</span><span>(</span><span>customerId</span><span>,</span> <span>Optional</span><span>.</span><span>of</span><span>(</span><span>customer</span><span>));</span><span>return</span> <span>customer</span><span>;</span><span>}</span> <span>catch</span> <span>(</span><span>CustomerNotFoundException</span> <span>e</span><span>)</span> <span>{</span><span>// Store negative result in cache to prevent repeated lookups</span><span>cache</span><span>.</span><span>put</span><span>(</span><span>customerId</span><span>,</span> <span>Optional</span><span>.</span><span>empty</span><span>());</span><span>throw</span> <span>e</span><span>;</span><span>}</span><span>}</span><span>public</span> <span>void</span> <span>updateCustomer</span><span>(</span><span>Customer</span> <span>customer</span><span>)</span> <span>{</span><span>// Write-through: update database first</span><span>repository</span><span>.</span><span>save</span><span>(</span><span>customer</span><span>);</span><span>// Then update cache</span><span>cache</span><span>.</span><span>put</span><span>(</span><span>customer</span><span>.</span><span>getId</span><span>(),</span> <span>Optional</span><span>.</span><span>of</span><span>(</span><span>customer</span><span>));</span><span>}</span><span>public</span> <span>void</span> <span>deleteCustomer</span><span>(</span><span>String</span> <span>customerId</span><span>)</span> <span>{</span><span>// Delete from database</span><span>repository</span><span>.</span><span>deleteById</span><span>(</span><span>customerId</span><span>);</span><span>// Remove from cache</span><span>cache</span><span>.</span><span>invalidate</span><span>(</span><span>customerId</span><span>);</span><span>}</span><span>}</span><span>@Service</span> <span>public</span> <span>class</span> <span>CacheAsideService</span> <span>{</span> <span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Optional</span><span><</span><span>Customer</span><span>>></span> <span>cache</span><span>;</span> <span>private</span> <span>final</span> <span>CustomerRepository</span> <span>repository</span><span>;</span> <span>public</span> <span>CacheAsideService</span><span>(</span><span>CustomerRepository</span> <span>repository</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>repository</span> <span>=</span> <span>repository</span><span>;</span> <span>this</span><span>.</span><span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>15</span><span>))</span> <span>.</span><span>build</span><span>();</span> <span>}</span> <span>public</span> <span>Customer</span> <span>getCustomer</span><span>(</span><span>String</span> <span>customerId</span><span>)</span> <span>{</span> <span>// Retrieve from cache, including negative caching with Optional</span> <span>Optional</span><span><</span><span>Customer</span><span>></span> <span>cachedResult</span> <span>=</span> <span>cache</span><span>.</span><span>getIfPresent</span><span>(</span><span>customerId</span><span>);</span> <span>if</span> <span>(</span><span>cachedResult</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span> <span>// Return cached result or throw exception for negative cache hit</span> <span>return</span> <span>cachedResult</span><span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>CustomerNotFoundException</span><span>(</span><span>customerId</span><span>));</span> <span>}</span> <span>try</span> <span>{</span> <span>// Cache miss - retrieve from database</span> <span>Customer</span> <span>customer</span> <span>=</span> <span>repository</span><span>.</span><span>findById</span><span>(</span><span>customerId</span><span>)</span> <span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>CustomerNotFoundException</span><span>(</span><span>customerId</span><span>));</span> <span>// Store positive result in cache</span> <span>cache</span><span>.</span><span>put</span><span>(</span><span>customerId</span><span>,</span> <span>Optional</span><span>.</span><span>of</span><span>(</span><span>customer</span><span>));</span> <span>return</span> <span>customer</span><span>;</span> <span>}</span> <span>catch</span> <span>(</span><span>CustomerNotFoundException</span> <span>e</span><span>)</span> <span>{</span> <span>// Store negative result in cache to prevent repeated lookups</span> <span>cache</span><span>.</span><span>put</span><span>(</span><span>customerId</span><span>,</span> <span>Optional</span><span>.</span><span>empty</span><span>());</span> <span>throw</span> <span>e</span><span>;</span> <span>}</span> <span>}</span> <span>public</span> <span>void</span> <span>updateCustomer</span><span>(</span><span>Customer</span> <span>customer</span><span>)</span> <span>{</span> <span>// Write-through: update database first</span> <span>repository</span><span>.</span><span>save</span><span>(</span><span>customer</span><span>);</span> <span>// Then update cache</span> <span>cache</span><span>.</span><span>put</span><span>(</span><span>customer</span><span>.</span><span>getId</span><span>(),</span> <span>Optional</span><span>.</span><span>of</span><span>(</span><span>customer</span><span>));</span> <span>}</span> <span>public</span> <span>void</span> <span>deleteCustomer</span><span>(</span><span>String</span> <span>customerId</span><span>)</span> <span>{</span> <span>// Delete from database</span> <span>repository</span><span>.</span><span>deleteById</span><span>(</span><span>customerId</span><span>);</span> <span>// Remove from cache</span> <span>cache</span><span>.</span><span>invalidate</span><span>(</span><span>customerId</span><span>);</span> <span>}</span> <span>}</span>@Service public class CacheAsideService { private final Cache<String, Optional<Customer>> cache; private final CustomerRepository repository; public CacheAsideService(CustomerRepository repository) { this.repository = repository; this.cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(Duration.ofMinutes(15)) .build(); } public Customer getCustomer(String customerId) { // Retrieve from cache, including negative caching with Optional Optional<Customer> cachedResult = cache.getIfPresent(customerId); if (cachedResult != null) { // Return cached result or throw exception for negative cache hit return cachedResult.orElseThrow(() -> new CustomerNotFoundException(customerId)); } try { // Cache miss - retrieve from database Customer customer = repository.findById(customerId) .orElseThrow(() -> new CustomerNotFoundException(customerId)); // Store positive result in cache cache.put(customerId, Optional.of(customer)); return customer; } catch (CustomerNotFoundException e) { // Store negative result in cache to prevent repeated lookups cache.put(customerId, Optional.empty()); throw e; } } public void updateCustomer(Customer customer) { // Write-through: update database first repository.save(customer); // Then update cache cache.put(customer.getId(), Optional.of(customer)); } public void deleteCustomer(String customerId) { // Delete from database repository.deleteById(customerId); // Remove from cache cache.invalidate(customerId); } }
Enter fullscreen mode Exit fullscreen mode
Key features of this implementation:
- Explicit caching logic gives fine-grained control
- Support for negative caching to prevent repeated lookups for missing items
- Clear write-through policy for updates
- Resilience to database failures using cached data
When implementing cache-aside, I consider these techniques:
- Grouping related operations with consistent caching behavior
- Adding batch operations to reduce cache chatter
- Using metrics to monitor hit rates and adjust cache parameters
- Implementing background refresh for critical data
Cache Invalidation Strategies
Managing cache invalidation is crucial for maintaining data consistency. I’ve implemented several strategies depending on system requirements:
<span>public</span> <span>class</span> <span>CacheInvalidationExample</span> <span>{</span><span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>localCache</span><span>;</span><span>private</span> <span>final</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>;</span><span>// Time-based invalidation</span><span>public</span> <span>void</span> <span>setupTimeBasedInvalidation</span><span>()</span> <span>{</span><span>Cache</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>10</span><span>))</span><span>.</span><span>build</span><span>();</span><span>}</span><span>// Event-based invalidation</span><span>@Transactional</span><span>public</span> <span>void</span> <span>updateEntity</span><span>(</span><span>Entity</span> <span>entity</span><span>)</span> <span>{</span><span>// Update database</span><span>repository</span><span>.</span><span>save</span><span>(</span><span>entity</span><span>);</span><span>// Explicitly invalidate cache</span><span>localCache</span><span>.</span><span>invalidate</span><span>(</span><span>entity</span><span>.</span><span>getId</span><span>());</span><span>// Publish invalidation event for other instances</span><span>redisTemplate</span><span>.</span><span>convertAndSend</span><span>(</span><span>"cache:invalidation"</span><span>,</span><span>new</span> <span>InvalidationEvent</span><span>(</span><span>"entity"</span><span>,</span> <span>entity</span><span>.</span><span>getId</span><span>()));</span><span>}</span><span>// Listener in other application instances</span><span>@RedisListener</span><span>(</span><span>topics</span> <span>=</span> <span>"cache:invalidation"</span><span>)</span><span>public</span> <span>void</span> <span>handleCacheInvalidation</span><span>(</span><span>InvalidationEvent</span> <span>event</span><span>)</span> <span>{</span><span>if</span> <span>(</span><span>"entity"</span><span>.</span><span>equals</span><span>(</span><span>event</span><span>.</span><span>getType</span><span>()))</span> <span>{</span><span>localCache</span><span>.</span><span>invalidate</span><span>(</span><span>event</span><span>.</span><span>getId</span><span>());</span><span>}</span><span>}</span><span>// Version-based invalidation</span><span>public</span> <span>Entity</span> <span>getEntityWithVersion</span><span>(</span><span>String</span> <span>id</span><span>)</span> <span>{</span><span>String</span> <span>cacheKey</span> <span>=</span> <span>id</span><span>;</span><span>CachedEntity</span> <span>cached</span> <span>=</span> <span>(</span><span>CachedEntity</span><span>)</span> <span>localCache</span><span>.</span><span>getIfPresent</span><span>(</span><span>cacheKey</span><span>);</span><span>// Check if cache entry exists and version matches</span><span>if</span> <span>(</span><span>cached</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span><span>String</span> <span>currentVersion</span> <span>=</span> <span>versionService</span><span>.</span><span>getCurrentVersion</span><span>(</span><span>"entity"</span><span>);</span><span>if</span> <span>(</span><span>currentVersion</span><span>.</span><span>equals</span><span>(</span><span>cached</span><span>.</span><span>getVersion</span><span>()))</span> <span>{</span><span>return</span> <span>cached</span><span>.</span><span>getEntity</span><span>();</span><span>}</span><span>}</span><span>// Fetch fresh data</span><span>Entity</span> <span>entity</span> <span>=</span> <span>repository</span><span>.</span><span>findById</span><span>(</span><span>id</span><span>)</span><span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>EntityNotFoundException</span><span>(</span><span>id</span><span>));</span><span>// Cache with current version</span><span>localCache</span><span>.</span><span>put</span><span>(</span><span>cacheKey</span><span>,</span> <span>new</span> <span>CachedEntity</span><span>(</span><span>entity</span><span>,</span><span>versionService</span><span>.</span><span>getCurrentVersion</span><span>(</span><span>"entity"</span><span>)</span><span>));</span><span>return</span> <span>entity</span><span>;</span><span>}</span><span>}</span><span>public</span> <span>class</span> <span>CacheInvalidationExample</span> <span>{</span> <span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>localCache</span><span>;</span> <span>private</span> <span>final</span> <span>RedisTemplate</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>redisTemplate</span><span>;</span> <span>// Time-based invalidation</span> <span>public</span> <span>void</span> <span>setupTimeBasedInvalidation</span><span>()</span> <span>{</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>expireAfterWrite</span><span>(</span><span>Duration</span><span>.</span><span>ofMinutes</span><span>(</span><span>10</span><span>))</span> <span>.</span><span>build</span><span>();</span> <span>}</span> <span>// Event-based invalidation</span> <span>@Transactional</span> <span>public</span> <span>void</span> <span>updateEntity</span><span>(</span><span>Entity</span> <span>entity</span><span>)</span> <span>{</span> <span>// Update database</span> <span>repository</span><span>.</span><span>save</span><span>(</span><span>entity</span><span>);</span> <span>// Explicitly invalidate cache</span> <span>localCache</span><span>.</span><span>invalidate</span><span>(</span><span>entity</span><span>.</span><span>getId</span><span>());</span> <span>// Publish invalidation event for other instances</span> <span>redisTemplate</span><span>.</span><span>convertAndSend</span><span>(</span><span>"cache:invalidation"</span><span>,</span> <span>new</span> <span>InvalidationEvent</span><span>(</span><span>"entity"</span><span>,</span> <span>entity</span><span>.</span><span>getId</span><span>()));</span> <span>}</span> <span>// Listener in other application instances</span> <span>@RedisListener</span><span>(</span><span>topics</span> <span>=</span> <span>"cache:invalidation"</span><span>)</span> <span>public</span> <span>void</span> <span>handleCacheInvalidation</span><span>(</span><span>InvalidationEvent</span> <span>event</span><span>)</span> <span>{</span> <span>if</span> <span>(</span><span>"entity"</span><span>.</span><span>equals</span><span>(</span><span>event</span><span>.</span><span>getType</span><span>()))</span> <span>{</span> <span>localCache</span><span>.</span><span>invalidate</span><span>(</span><span>event</span><span>.</span><span>getId</span><span>());</span> <span>}</span> <span>}</span> <span>// Version-based invalidation</span> <span>public</span> <span>Entity</span> <span>getEntityWithVersion</span><span>(</span><span>String</span> <span>id</span><span>)</span> <span>{</span> <span>String</span> <span>cacheKey</span> <span>=</span> <span>id</span><span>;</span> <span>CachedEntity</span> <span>cached</span> <span>=</span> <span>(</span><span>CachedEntity</span><span>)</span> <span>localCache</span><span>.</span><span>getIfPresent</span><span>(</span><span>cacheKey</span><span>);</span> <span>// Check if cache entry exists and version matches</span> <span>if</span> <span>(</span><span>cached</span> <span>!=</span> <span>null</span><span>)</span> <span>{</span> <span>String</span> <span>currentVersion</span> <span>=</span> <span>versionService</span><span>.</span><span>getCurrentVersion</span><span>(</span><span>"entity"</span><span>);</span> <span>if</span> <span>(</span><span>currentVersion</span><span>.</span><span>equals</span><span>(</span><span>cached</span><span>.</span><span>getVersion</span><span>()))</span> <span>{</span> <span>return</span> <span>cached</span><span>.</span><span>getEntity</span><span>();</span> <span>}</span> <span>}</span> <span>// Fetch fresh data</span> <span>Entity</span> <span>entity</span> <span>=</span> <span>repository</span><span>.</span><span>findById</span><span>(</span><span>id</span><span>)</span> <span>.</span><span>orElseThrow</span><span>(()</span> <span>-></span> <span>new</span> <span>EntityNotFoundException</span><span>(</span><span>id</span><span>));</span> <span>// Cache with current version</span> <span>localCache</span><span>.</span><span>put</span><span>(</span><span>cacheKey</span><span>,</span> <span>new</span> <span>CachedEntity</span><span>(</span> <span>entity</span><span>,</span> <span>versionService</span><span>.</span><span>getCurrentVersion</span><span>(</span><span>"entity"</span><span>)</span> <span>));</span> <span>return</span> <span>entity</span><span>;</span> <span>}</span> <span>}</span>public class CacheInvalidationExample { private final Cache<String, Object> localCache; private final RedisTemplate<String, Object> redisTemplate; // Time-based invalidation public void setupTimeBasedInvalidation() { Cache<String, Object> cache = Caffeine.newBuilder() .expireAfterWrite(Duration.ofMinutes(10)) .build(); } // Event-based invalidation @Transactional public void updateEntity(Entity entity) { // Update database repository.save(entity); // Explicitly invalidate cache localCache.invalidate(entity.getId()); // Publish invalidation event for other instances redisTemplate.convertAndSend("cache:invalidation", new InvalidationEvent("entity", entity.getId())); } // Listener in other application instances @RedisListener(topics = "cache:invalidation") public void handleCacheInvalidation(InvalidationEvent event) { if ("entity".equals(event.getType())) { localCache.invalidate(event.getId()); } } // Version-based invalidation public Entity getEntityWithVersion(String id) { String cacheKey = id; CachedEntity cached = (CachedEntity) localCache.getIfPresent(cacheKey); // Check if cache entry exists and version matches if (cached != null) { String currentVersion = versionService.getCurrentVersion("entity"); if (currentVersion.equals(cached.getVersion())) { return cached.getEntity(); } } // Fetch fresh data Entity entity = repository.findById(id) .orElseThrow(() -> new EntityNotFoundException(id)); // Cache with current version localCache.put(cacheKey, new CachedEntity( entity, versionService.getCurrentVersion("entity") )); return entity; } }
Enter fullscreen mode Exit fullscreen mode
I’ve found that combining these approaches works best:
- Time-based expiration as a safety mechanism for all caches
- Event-based invalidation for immediate consistency on updates
- Version-based invalidation for bulk changes affecting multiple entries
Monitoring and Optimization
To ensure caches are effective, I implement comprehensive monitoring:
<span>@Service</span><span>public</span> <span>class</span> <span>CacheMonitoringService</span> <span>{</span><span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>cache</span><span>;</span><span>private</span> <span>final</span> <span>MeterRegistry</span> <span>meterRegistry</span><span>;</span><span>public</span> <span>CacheMonitoringService</span><span>(</span><span>MeterRegistry</span> <span>meterRegistry</span><span>)</span> <span>{</span><span>this</span><span>.</span><span>meterRegistry</span> <span>=</span> <span>meterRegistry</span><span>;</span><span>this</span><span>.</span><span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span><span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span><span>.</span><span>recordStats</span><span>()</span><span>.</span><span>build</span><span>();</span><span>// Register metrics</span><span>registerCacheMetrics</span><span>();</span><span>}</span><span>private</span> <span>void</span> <span>registerCacheMetrics</span><span>()</span> <span>{</span><span>// Register hit rate gauge</span><span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.hit.ratio"</span><span>,</span><span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span><span>cache</span><span>.</span><span>stats</span><span>(),</span><span>stats</span> <span>-></span> <span>stats</span><span>.</span><span>hitRate</span><span>());</span><span>// Register size gauge</span><span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.size"</span><span>,</span><span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span><span>cache</span><span>,</span><span>c</span> <span>-></span> <span>c</span><span>.</span><span>estimatedSize</span><span>());</span><span>// Register hit count</span><span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.hits"</span><span>,</span><span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span><span>cache</span><span>.</span><span>stats</span><span>(),</span><span>stats</span> <span>-></span> <span>stats</span><span>.</span><span>hitCount</span><span>());</span><span>// Register miss count</span><span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.misses"</span><span>,</span><span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span><span>cache</span><span>.</span><span>stats</span><span>(),</span><span>stats</span> <span>-></span> <span>stats</span><span>.</span><span>missCount</span><span>());</span><span>}</span><span>}</span><span>@Service</span> <span>public</span> <span>class</span> <span>CacheMonitoringService</span> <span>{</span> <span>private</span> <span>final</span> <span>Cache</span><span><</span><span>String</span><span>,</span> <span>Object</span><span>></span> <span>cache</span><span>;</span> <span>private</span> <span>final</span> <span>MeterRegistry</span> <span>meterRegistry</span><span>;</span> <span>public</span> <span>CacheMonitoringService</span><span>(</span><span>MeterRegistry</span> <span>meterRegistry</span><span>)</span> <span>{</span> <span>this</span><span>.</span><span>meterRegistry</span> <span>=</span> <span>meterRegistry</span><span>;</span> <span>this</span><span>.</span><span>cache</span> <span>=</span> <span>Caffeine</span><span>.</span><span>newBuilder</span><span>()</span> <span>.</span><span>maximumSize</span><span>(</span><span>10_000</span><span>)</span> <span>.</span><span>recordStats</span><span>()</span> <span>.</span><span>build</span><span>();</span> <span>// Register metrics</span> <span>registerCacheMetrics</span><span>();</span> <span>}</span> <span>private</span> <span>void</span> <span>registerCacheMetrics</span><span>()</span> <span>{</span> <span>// Register hit rate gauge</span> <span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.hit.ratio"</span><span>,</span> <span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span> <span>cache</span><span>.</span><span>stats</span><span>(),</span> <span>stats</span> <span>-></span> <span>stats</span><span>.</span><span>hitRate</span><span>());</span> <span>// Register size gauge</span> <span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.size"</span><span>,</span> <span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span> <span>cache</span><span>,</span> <span>c</span> <span>-></span> <span>c</span><span>.</span><span>estimatedSize</span><span>());</span> <span>// Register hit count</span> <span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.hits"</span><span>,</span> <span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span> <span>cache</span><span>.</span><span>stats</span><span>(),</span> <span>stats</span> <span>-></span> <span>stats</span><span>.</span><span>hitCount</span><span>());</span> <span>// Register miss count</span> <span>meterRegistry</span><span>.</span><span>gauge</span><span>(</span><span>"cache.misses"</span><span>,</span> <span>Tags</span><span>.</span><span>of</span><span>(</span><span>"name"</span><span>,</span> <span>"mainCache"</span><span>),</span> <span>cache</span><span>.</span><span>stats</span><span>(),</span> <span>stats</span> <span>-></span> <span>stats</span><span>.</span><span>missCount</span><span>());</span> <span>}</span> <span>}</span>@Service public class CacheMonitoringService { private final Cache<String, Object> cache; private final MeterRegistry meterRegistry; public CacheMonitoringService(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; this.cache = Caffeine.newBuilder() .maximumSize(10_000) .recordStats() .build(); // Register metrics registerCacheMetrics(); } private void registerCacheMetrics() { // Register hit rate gauge meterRegistry.gauge("cache.hit.ratio", Tags.of("name", "mainCache"), cache.stats(), stats -> stats.hitRate()); // Register size gauge meterRegistry.gauge("cache.size", Tags.of("name", "mainCache"), cache, c -> c.estimatedSize()); // Register hit count meterRegistry.gauge("cache.hits", Tags.of("name", "mainCache"), cache.stats(), stats -> stats.hitCount()); // Register miss count meterRegistry.gauge("cache.misses", Tags.of("name", "mainCache"), cache.stats(), stats -> stats.missCount()); } }
Enter fullscreen mode Exit fullscreen mode
Based on metrics, I optimize cache parameters:
- Adjust cache size based on hit rate and memory usage
- Tune expiration times based on data freshness requirements
- Implement pre-warming for critical caches to prevent cold starts
- Add specialized caches for hot spots identified in the application
Conclusion
Effective caching is a balancing act between memory usage, performance, and data consistency. I’ve found that combining multiple strategies—Caffeine for local caching, Redis for distributed scenarios, and attribute-level caching for efficiency—provides the best results.
When implementing caching, I focus on these principles:
- Cache data close to where it’s used to minimize latency
- Set appropriate time-to-live values based on data volatility
- Implement precise invalidation mechanisms
- Monitor cache effectiveness and adjust accordingly
- Consider the entire system architecture when designing caching strategies
With thoughtful implementation of these caching techniques, I’ve achieved performance improvements ranging from 10x to 100x for read-heavy operations while maintaining reasonable memory consumption and data consistency.
The key to successful caching is understanding your application’s access patterns and data characteristics, then applying targeted caching strategies rather than attempting to cache everything indiscriminately.
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
原文链接:High-Performance Java Caching: Techniques for Faster Applications
暂无评论内容