Backend Development 16 min read

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.

JD Tech Talk
JD Tech Talk
JD Tech Talk
Using Spring Cache Annotations for Efficient Redis‑Backed Caching in Java Applications

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=300

JUnit 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.

backendJavaperformanceCacheRedisSpringAnnotations
JD Tech Talk
Written by

JD Tech Talk

Official JD Tech public account delivering best practices and technology innovation.

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.