#1
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:
  1. Application checks the local cache (L1) first.
  2. If data is not found, it checks Redis (L2).
  3. If still missing, it queries the database.
  4. 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 Caffeine
Maven 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
#ads

image quote pre code