Using Spring Cache Annotations for Efficient Redis‑Backed Caching in Java Applications
This article explains how to use Spring's built‑in cache annotations such as @EnableCaching, @Cacheable, @CachePut, and @CacheEvict to implement Redis‑backed caching in a Spring Boot application, covering configuration, core code, testing, and practical considerations for cache consistency and expiration.
Cache is a common technique in daily development to reduce database read pressure and improve system query performance. It is typically used for data that changes infrequently but is accessed frequently, such as configuration, product, or user information. The usual approach is to read from the database once, store the result in the cache with an expiration time, and synchronize cache updates and deletions with database operations.
Spring already provides powerful cache annotation capabilities, so there is no need to create custom annotations. The key Spring cache annotations are @EnableCaching, @Cacheable, @CachePut, and @CacheEvict.
Key Spring Cache Annotations
Annotation
Description
@EnableCaching
Enables cache support; must be placed on a configuration class for other cache annotations to take effect.
@Cacheable
Marks a method or class as cacheable; the method result is stored in the cache and reused for identical parameters. Attributes:
value
(or cacheNames) specifies the cache name,
key
defines the cache key (SpEL supported), and
condition
controls whether caching is applied.
@CachePut
Ensures the method is always executed and its result is placed into the cache, typically used for cache updates.
@CacheEvict
Clears cache entries when the annotated method is invoked, commonly used after deletions to keep cache and database consistent.
Project Dependency
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>spring-cache</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>Cache Configuration Class
package com.java.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.support.NoOpCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.time.Duration;
/**
* Redis cache configuration class
*/
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Value("${cache.enable:false}")
private Boolean cacheEnable;
@Value("${cache.ttl:120}")
private Long cacheTtl;
@Autowired
private StringRedisTemplate redisTemplate;
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
if (cacheEnable) {
RedisCacheConfiguration config = instanceConfig();
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.transactionAware()
.build();
}
return new NoOpCacheManager();
}
private RedisCacheConfiguration instanceConfig() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(cacheTtl))
.disableCachingNullValues();
}
}Domain Entity (User)
package com.java.demo.model;
import java.io.Serializable;
/**
* User entity
*/
public class User implements Serializable {
private String userId;
private String userName;
public User() {}
public User(String userId, String userName) {
this.userId = userId;
this.userName = userName;
}
@Override
public String toString() {
return String.format("[userId:%s,userName:%s]", userId, userName);
}
// getters and setters omitted for brevity
}Service Interface and Implementation with Cache Annotations
package com.java.demo.service;
import com.java.demo.model.User;
public interface UserService {
User getUserById(String userId);
User updateUser(User user);
void deleteUser(String userId);
} package com.java.demo.service.impl;
import com.java.demo.model.User;
import com.java.demo.service.UserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Component;
@Component
public class UserServiceImpl implements UserService {
private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
@Override
@Cacheable(cacheNames = "users", key = "#userId")
public User getUserById(String userId) {
logger.info("Calling getUserById, param:{}", userId);
// Simulate DB fetch
return new User("123", "li lei");
}
@Override
@CachePut(cacheNames = "users", key = "#user.userId")
public User updateUser(User user) {
logger.info("Calling updateUser, param:{}", user);
return user;
}
@Override
@CacheEvict(cacheNames = "users", key = "#userId")
public void deleteUser(String userId) {
logger.info("Calling deleteUser, param:{}", userId);
}
}Application Startup Class
package com.java.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("com.java.demo")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
}Cache‑related Properties (application.properties)
spring.redis.host=//redis address
spring.redis.database=0
spring.redis.port=//redis port
spring.redis.password=//redis password
spring.redis.timeout=5000
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=1
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=3000
cache.enable=true
cache.ttl=300JUnit Test Case Demonstrating Cache Behavior
package com.java.demo;
import com.java.demo.model.User;
import com.java.demo.service.UserService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CacheTest {
private static final Logger logger = LoggerFactory.getLogger(CacheTest.class);
@Autowired
private UserService userService;
@Test
public void testCache() {
// First call – cache miss
logger.info("1.user:{}", userService.getUserById("123"));
// Second call – cache hit
logger.info("2.user:{}", userService.getUserById("123"));
// Update cache
userService.updateUser(new User("123", "zhang hua"));
// Third call – should read updated value from cache
logger.info("3.user:{}", userService.getUserById("123"));
// Delete cache entry
userService.deleteUser("123");
logger.info("test finish!");
}
}Observations
The first invocation loads data from the method and stores it in Redis. The second invocation retrieves the result directly from the cache, as shown by the log output. Updating the user with @CachePut synchronizes the cache, and @CacheEvict removes the entry to keep cache and database consistent.
One limitation of @EnableCaching is that it does not provide a direct attribute for cache expiration; the TTL must be configured via the CacheManager (as demonstrated with cache.ttl ). This design gives developers flexibility to customize expiration policies, but it also means additional configuration is required for time‑based eviction.
Overall, leveraging Spring's cache annotations decouples caching logic from business code, resulting in cleaner, more maintainable applications and improved development efficiency.
JD Tech Talk
Official JD Tech public account delivering best practices and technology innovation.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.