Backend Development 17 min read

Implementing Two-Level Cache with Caffeine and Redis in Spring Boot

This article explains the design and implementation of a two‑level caching architecture using local Caffeine cache as L1 and remote Redis as L2 in Spring Boot, covering manual cache handling, annotation‑based management with Spring Cache, and a custom annotation with AOP to minimize code intrusion.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Implementing Two-Level Cache with Caffeine and Redis in Spring Boot

In high‑performance service architecture, caching is essential; remote caches like Redis or Memcached reduce database load, but combining them with a local cache (Caffeine or Guava) forms a two‑level cache that further improves response time.

Advantages: local cache provides ultra‑fast memory access and reduces network I/O; however, consistency between L1, L2 and the database must be maintained, especially in distributed environments where cache invalidation is required.

Preparation

Add the following Maven dependencies to a Spring Boot project:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.8.1</version>
</dependency>

Configure Redis connection in application.yml :

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0
    timeout: 10000ms
    lettuce:
      pool:
        max-active: 8
        max-wait: -1ms
        max-idle: 8
        min-idle: 0

V1 – Manual Two‑Level Cache

Define a Caffeine bean:

@Configuration
public class CaffeineConfig {
    @Bean
    public Cache
caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .build();
    }
}

Use the cache directly in service methods, e.g. query:

public Order getOrderById(Long id) {
    String key = CacheConstant.ORDER + id;
    Order order = (Order) cache.get(key, k -> {
        Object obj = redisTemplate.opsForValue().get(k);
        if (obj != null) {
            log.info("get data from redis");
            return obj;
        }
        log.info("get data from database");
        Order dbOrder = orderMapper.selectOne(new LambdaQueryWrapper
().eq(Order::getId, id));
        redisTemplate.opsForValue().set(k, dbOrder, 120, TimeUnit.SECONDS);
        return dbOrder;
    });
    return order;
}

Update and delete operations manually synchronize both Caffeine and Redis caches.

V2 – Spring Cache Annotations

Configure a CaffeineCacheManager and enable caching with @EnableCaching . Then annotate service methods:

@Cacheable(value = "order", key = "#id")
public Order getOrderById(Long id) { … }
@CachePut(cacheNames = "order", key = "#order.id")
public Order updateOrder(Order order) { … }
@CacheEvict(cacheNames = "order", key = "#id")
public void deleteOrder(Long id) { … }

Spring handles the local cache automatically, while Redis updates remain explicit.

V3 – Custom Annotation with AOP

Define a @DoubleCache annotation that specifies cache name, key (Spring EL), L2 timeout and operation type (FULL, PUT, DELETE). Implement an aspect that parses the EL key, builds the real cache key, and performs read‑through, write‑through or eviction logic against both Caffeine and Redis.

Service methods become concise:

@DoubleCache(cacheName = "order", key = "#id", type = CacheType.FULL)
public Order getOrderById(Long id) { … }
@DoubleCache(cacheName = "order", key = "#order.id", type = CacheType.PUT)
public Order updateOrder(Order order) { … }
@DoubleCache(cacheName = "order", key = "#id", type = CacheType.DELETE)
public void deleteOrder(Long id) { … }

Conclusion

The article demonstrates three approaches—manual two‑level cache, Spring’s annotation‑driven cache, and a custom AOP‑based annotation—to reduce cache‑related code intrusion in Spring Boot applications, while highlighting consistency, expiration, and concurrency considerations.

aopredisSpring BootCaffeineTwo-Level CacheCache Annotation
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.