Spring Boot 8 min read

Redis Caching with Spring Boot: Complete Implementation Guide

Master Redis caching in Spring Boot applications. Learn cache configuration, annotations, TTL management, and performance optimization techniques.

MR

Moshiour Rahman

Advertisement

Why Caching?

Caching dramatically improves application performance by storing frequently accessed data in memory. Redis is an in-memory data store perfect for caching.

Benefits

Without CacheWith Cache
Database hit every requestMemory-fast responses
Higher latencySub-millisecond access
More server loadReduced database load
Higher costsBetter scalability

Setup

Dependencies

<dependencies>
    <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>
</dependencies>

Docker Compose for Redis

version: '3.8'
services:
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes

volumes:
  redis_data:

Configuration

# application.yml
spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: # optional
      timeout: 2000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0

  cache:
    type: redis
    redis:
      time-to-live: 3600000  # 1 hour in milliseconds
      cache-null-values: false

Basic Configuration

Enable Caching

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1))
                .serializeKeysWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
                )
                .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
                )
                .disableCachingNullValues();

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

Multiple Cache Configurations

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        // Default configuration
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeValuesWith(
                    RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
                );

        // Custom configurations for specific caches
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();

        cacheConfigurations.put("users", defaultConfig.entryTtl(Duration.ofHours(1)));
        cacheConfigurations.put("products", defaultConfig.entryTtl(Duration.ofMinutes(15)));
        cacheConfigurations.put("sessions", defaultConfig.entryTtl(Duration.ofMinutes(30)));

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(defaultConfig)
                .withInitialCacheConfigurations(cacheConfigurations)
                .build();
    }
}

Cache Annotations

@Cacheable

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;

    // Cache the result with user ID as key
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        log.info("Fetching user from database: {}", id);
        return userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException("User not found: " + id));
    }

    // Cache with custom key
    @Cacheable(value = "users", key = "#email")
    public User getUserByEmail(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UserNotFoundException("User not found"));
    }

    // Cache with SpEL expression
    @Cacheable(value = "users", key = "'user_' + #id + '_' + #includeDetails")
    public UserDTO getUserDetails(Long id, boolean includeDetails) {
        // Implementation
    }

    // Conditional caching
    @Cacheable(value = "users", key = "#id", condition = "#id > 0")
    public User getUserConditional(Long id) {
        return userRepository.findById(id).orElse(null);
    }

    // Cache unless result is null
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserUnlessNull(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

@CachePut

@Service
public class UserService {

    // Always execute method and update cache
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        log.info("Updating user: {}", user.getId());
        return userRepository.save(user);
    }

    @CachePut(value = "users", key = "#result.id")
    public User createUser(CreateUserRequest request) {
        User user = User.builder()
                .name(request.getName())
                .email(request.getEmail())
                .build();
        return userRepository.save(user);
    }
}

@CacheEvict

@Service
public class UserService {

    // Remove single entry from cache
    @CacheEvict(value = "users", key = "#id")
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }

    // Clear entire cache
    @CacheEvict(value = "users", allEntries = true)
    public void clearUserCache() {
        log.info("User cache cleared");
    }

    // Evict before method execution
    @CacheEvict(value = "users", key = "#id", beforeInvocation = true)
    public void deleteUserBeforeInvocation(Long id) {
        // Even if this throws exception, cache entry is removed
        userRepository.deleteById(id);
    }
}

@Caching (Multiple Operations)

@Service
public class UserService {

    @Caching(
        put = {
            @CachePut(value = "users", key = "#result.id"),
            @CachePut(value = "usersByEmail", key = "#result.email")
        },
        evict = {
            @CacheEvict(value = "allUsers", allEntries = true)
        }
    )
    public User updateUser(User user) {
        return userRepository.save(user);
    }

    @Caching(evict = {
        @CacheEvict(value = "users", key = "#id"),
        @CacheEvict(value = "usersByEmail", allEntries = true),
        @CacheEvict(value = "allUsers", allEntries = true)
    })
    public void deleteUser(Long id) {
        userRepository.deleteById(id);
    }
}

Using RedisTemplate

Configuration

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        // Key serializer
        template.setKeySerializer(new StringRedisSerializer());

        // Value serializer
        Jackson2JsonRedisSerializer<Object> jsonSerializer =
                new Jackson2JsonRedisSerializer<>(Object.class);
        template.setValueSerializer(jsonSerializer);

        // Hash serializers
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

Direct Redis Operations

@Service
@RequiredArgsConstructor
public class CacheService {

    private final RedisTemplate<String, Object> redisTemplate;

    // String operations
    public void setValue(String key, Object value, Duration ttl) {
        redisTemplate.opsForValue().set(key, value, ttl);
    }

    public Object getValue(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    // Check if key exists
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    // Delete key
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    // Set expiration
    public void setExpiration(String key, Duration ttl) {
        redisTemplate.expire(key, ttl);
    }

    // List operations
    public void addToList(String key, Object value) {
        redisTemplate.opsForList().rightPush(key, value);
    }

    public List<Object> getList(String key) {
        return redisTemplate.opsForList().range(key, 0, -1);
    }

    // Set operations
    public void addToSet(String key, Object... values) {
        redisTemplate.opsForSet().add(key, values);
    }

    public Set<Object> getSetMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    // Hash operations
    public void setHash(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public Object getHashValue(String key, String hashKey) {
        return redisTemplate.opsForHash().get(key, hashKey);
    }

    public Map<Object, Object> getHash(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
}

Session Caching

Store Sessions in Redis

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)  // 30 minutes
public class SessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("SESSIONID");
        serializer.setCookiePath("/");
        serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
        return serializer;
    }
}

Cache Patterns

Cache-Aside Pattern

@Service
@RequiredArgsConstructor
public class ProductService {

    private final ProductRepository productRepository;
    private final RedisTemplate<String, Product> redisTemplate;

    private static final String CACHE_PREFIX = "product:";
    private static final Duration CACHE_TTL = Duration.ofHours(1);

    public Product getProduct(Long id) {
        String key = CACHE_PREFIX + id;

        // Try to get from cache
        Product cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return cached;
        }

        // Get from database
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));

        // Store in cache
        redisTemplate.opsForValue().set(key, product, CACHE_TTL);

        return product;
    }

    public Product updateProduct(Long id, UpdateProductRequest request) {
        Product product = productRepository.findById(id)
                .orElseThrow(() -> new ProductNotFoundException(id));

        product.setName(request.getName());
        product.setPrice(request.getPrice());
        Product saved = productRepository.save(product);

        // Update cache
        String key = CACHE_PREFIX + id;
        redisTemplate.opsForValue().set(key, saved, CACHE_TTL);

        return saved;
    }

    public void deleteProduct(Long id) {
        productRepository.deleteById(id);

        // Remove from cache
        String key = CACHE_PREFIX + id;
        redisTemplate.delete(key);
    }
}

Write-Through Pattern

@Service
@RequiredArgsConstructor
public class UserService {

    @CachePut(value = "users", key = "#result.id")
    @Transactional
    public User createUser(CreateUserRequest request) {
        // Both database and cache are updated together
        User user = User.builder()
                .name(request.getName())
                .email(request.getEmail())
                .build();
        return userRepository.save(user);
    }
}

Cache Warming

Preload Cache on Startup

@Component
@RequiredArgsConstructor
@Slf4j
public class CacheWarmer implements ApplicationRunner {

    private final ProductService productService;
    private final ProductRepository productRepository;

    @Override
    public void run(ApplicationArguments args) {
        log.info("Warming up product cache...");

        // Load most popular products into cache
        List<Product> popularProducts = productRepository.findTop100ByOrderByViewCountDesc();

        popularProducts.forEach(product -> {
            productService.getProductById(product.getId());  // This will cache the product
        });

        log.info("Cache warming completed. {} products cached.", popularProducts.size());
    }
}

Scheduled Cache Refresh

@Component
@RequiredArgsConstructor
public class CacheRefreshScheduler {

    private final CacheManager cacheManager;
    private final ProductService productService;

    @Scheduled(fixedRate = 3600000)  // Every hour
    public void refreshProductCache() {
        Cache cache = cacheManager.getCache("products");
        if (cache != null) {
            cache.clear();
        }
        // Re-warm cache
        productService.warmCache();
    }
}

Error Handling

Graceful Degradation

@Service
@RequiredArgsConstructor
@Slf4j
public class ResilientCacheService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final UserRepository userRepository;

    public User getUser(Long id) {
        String key = "user:" + id;

        try {
            User cached = (User) redisTemplate.opsForValue().get(key);
            if (cached != null) {
                return cached;
            }
        } catch (Exception e) {
            log.warn("Redis unavailable, falling back to database: {}", e.getMessage());
        }

        // Fallback to database
        User user = userRepository.findById(id)
                .orElseThrow(() -> new UserNotFoundException(id));

        // Try to cache
        try {
            redisTemplate.opsForValue().set(key, user, Duration.ofHours(1));
        } catch (Exception e) {
            log.warn("Failed to cache user: {}", e.getMessage());
        }

        return user;
    }
}

Custom Cache Error Handler

@Configuration
@EnableCaching
public class CacheConfig extends CachingConfigurerSupport {

    @Override
    public CacheErrorHandler errorHandler() {
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
                log.warn("Cache get error for key {}: {}", key, e.getMessage());
            }

            @Override
            public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
                log.warn("Cache put error for key {}: {}", key, e.getMessage());
            }

            @Override
            public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
                log.warn("Cache evict error for key {}: {}", key, e.getMessage());
            }

            @Override
            public void handleCacheClearError(RuntimeException e, Cache cache) {
                log.warn("Cache clear error: {}", e.getMessage());
            }
        };
    }
}

Monitoring

Cache Statistics

@RestController
@RequestMapping("/api/cache")
@RequiredArgsConstructor
public class CacheController {

    private final RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/stats")
    public Map<String, Object> getCacheStats() {
        Properties info = redisTemplate.getConnectionFactory()
                .getConnection()
                .info();

        Map<String, Object> stats = new HashMap<>();
        stats.put("used_memory", info.getProperty("used_memory_human"));
        stats.put("connected_clients", info.getProperty("connected_clients"));
        stats.put("total_connections", info.getProperty("total_connections_received"));
        stats.put("keyspace_hits", info.getProperty("keyspace_hits"));
        stats.put("keyspace_misses", info.getProperty("keyspace_misses"));

        return stats;
    }

    @GetMapping("/keys")
    public Set<String> getKeys(@RequestParam(defaultValue = "*") String pattern) {
        return redisTemplate.keys(pattern);
    }
}

Summary

AnnotationPurpose
@CacheableCache method result
@CachePutUpdate cache
@CacheEvictRemove from cache
@CachingMultiple operations

Redis caching dramatically improves application performance when implemented correctly with proper TTL management and error handling.

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.