Caching is one of the most effective ways to improve application performance. In large systems, a single cache layer may not be enough. A
multi-level caching architecture combines different types of caches to reduce latency and minimize database load.
In this article, you will learn how to design a
two-level cache system using local cache and Redis in a Spring Boot application.
1. What Is Multi-Level Caching
Multi-level caching uses multiple cache layers.
Typical architecture:
Client
↓
Spring Boot Application
↓
L1 Cache (Local Memory)
↓
L2 Cache (Redis)
↓
Database
How it works:
- Application checks the local cache (L1) first.
- If data is not found, it checks Redis (L2).
- If still missing, it queries the database.
- Results are stored in both cache levels.
This approach reduces response time significantly.
2. Benefits of Multi-Level Caching
Advantages include:
- Faster response times
- Reduced Redis network calls
- Lower database load
- Better scalability for high-traffic systems
Local cache access is extremely fast because it runs inside the application memory.
3. Implementing Local Cache (L1)
A popular local cache library is
CaffeineMaven dependency:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
Example configuration:
@Bean
public Cache<String, Object> localCache() {
return Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
This cache runs directly in application memory.
4. Using Redis as L2 Cache
Redis acts as a shared distributed cache across multiple instances.
Example Redis configuration:
spring.redis.host=localhost
spring.redis.port=6379
spring.cache.type=redis
Redis ensures cached data is available across all application servers.
5. Multi-Level Cache Lookup Logic
Example service implementation:
@Service
public class ProductService {
@Autowired
private Cache<String, Object> localCache;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object getProduct(String id) {
Object value = localCache.getIfPresent(id);
if (value != null) {
return value;
}
value = redisTemplate.opsForValue().get(id);
if (value != null) {
localCache.put(id, value);
return value;
}
value = fetchFromDatabase(id);
redisTemplate.opsForValue().set(id, value);
localCache.put(id, value);
return value;
}
private Object fetchFromDatabase(String id) {
return "Product " + id;
}
}
This logic checks cache layers sequentially.
6. Cache Invalidation Strategy
When data changes, both caches must be updated.
Example:
public void updateProduct(String id, Object value) {
redisTemplate.opsForValue().set(id, value);
localCache.put(id, value);
}
Without proper invalidation, the application may return outdated data.
7. Avoiding Cache Inconsistency
In distributed systems, local caches can become inconsistent.
Solutions:
- Use short TTL for local cache
- Publish cache invalidation events
- Use Redis Pub/Sub to notify instances
Example idea:
Service A updates data
↓
Redis Pub/Sub event
↓
All services clear local cache
8. Monitoring Cache Performance
Monitor key metrics:
- L1 cache hit rate
- L2 cache hit rate
- Database query frequency
- Cache memory usage
Spring Boot Actuator helps expose metrics.
management.endpoints.web.exposure.include=metrics
image quote pre code