Skip to main content
ClaudeWave
Skill85 repo starsupdated 6d ago

spring-data-redis

This Spring Data Redis skill configures caching in Spring Boot applications using Redis as the backend store. It provides a complete setup including RedisTemplate bean configuration with JSON serialization, a RedisCacheManager with configurable TTLs for different cache regions, and declarative caching annotations like @Cacheable and @CachePut. Use this when building applications that need to reduce database load by caching frequently accessed data with automatic expiration policies.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/rrezartprebreza/spring-boot-skills /tmp/spring-data-redis && cp -r /tmp/spring-data-redis/skills/spring-data-redis ~/.claude/skills/spring-data-redis
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# Spring Data Redis

## Dependencies

```xml
<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>
```

## Configuration

```java
@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // JSON, not Java serialize
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        return template;
    }

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

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withCacheConfiguration("orders", config.entryTtl(Duration.ofMinutes(5)))
            .withCacheConfiguration("products", config.entryTtl(Duration.ofHours(1)))
            .build();
    }
}
```

## Key Naming Convention

```
{app}:{domain}:{id}          → orders:order:uuid-here
{app}:{domain}:list:{filter} → orders:order:list:status:PENDING
{app}:session:{userId}       → orders:session:uuid-here
{app}:ratelimit:{ip}         → orders:ratelimit:192.168.1.1
```

## @Cacheable — Declarative Caching

```java
@Service
@RequiredArgsConstructor
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public ProductResponse findById(UUID id) {
        return productRepository.findById(id)
            .map(ProductResponse::from)
            .orElseThrow(() -> new EntityNotFoundException("Product not found: " + id));
    }

    @CachePut(value = "products", key = "#result.id")  // update cache after write
    @Transactional
    public ProductResponse update(UUID id, UpdateProductRequest request) {
        Product product = productRepository.findById(id).orElseThrow();
        product.update(request);
        return ProductResponse.from(productRepository.save(product));
    }

    @CacheEvict(value = "products", key = "#id")  // invalidate on delete
    @Transactional
    public void delete(UUID id) {
        productRepository.deleteById(id);
    }

    @CacheEvict(value = "products", allEntries = true)  // clear all
    public void clearCache() {}
}
```

## Manual Cache-Aside Pattern

```java
@Service
@RequiredArgsConstructor
public class OrderCacheService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;
    private static final Duration TTL = Duration.ofMinutes(5);

    public Optional<OrderResponse> get(UUID orderId) {
        String key = "orders:order:" + orderId;
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached == null) return Optional.empty();
        return Optional.of(objectMapper.convertValue(cached, OrderResponse.class));
    }

    public void put(OrderResponse order) {
        String key = "orders:order:" + order.id();
        redisTemplate.opsForValue().set(key, order, TTL);
    }

    public void evict(UUID orderId) {
        redisTemplate.delete("orders:order:" + orderId);
    }
}
```

## Rate Limiting with Redis

```java
@Component
@RequiredArgsConstructor
public class RateLimiter {

    private final RedisTemplate<String, String> redisTemplate;

    public boolean isAllowed(String identifier, int maxRequests, Duration window) {
        String key = "ratelimit:" + identifier;
        Long count = redisTemplate.opsForValue().increment(key);
        if (count == 1) {
            redisTemplate.expire(key, window);
        }
        return count <= maxRequests;
    }
}
```

## application.yml

```yaml
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      timeout: 2000ms
      lettuce:
        pool:
          max-active: 10
          max-idle: 5
          min-idle: 2
  cache:
    type: redis
```

## Cache Stampede

When a hot key expires, every concurrent request misses at once and they all hammer the DB to recompute
the same value (the "thundering herd"). For expensive, high-traffic loads, let one caller compute while
the rest wait:

```java
// sync = true — only one thread computes the value; others block on it
@Cacheable(value = "products", key = "#id", sync = true)
public ProductResponse findById(UUID id) { ... }
```

`sync = true` serializes recomputation per key within a single instance. For a fleet-wide guarantee,
add a short Redis lock (`SETNX` with a TTL) around the recompute. Pair with jittered TTLs so a batch of
keys written together doesn't all expire on the same second.

## Gotchas
- Agent uses Java serialization for values — always use JSON (`GenericJackson2JsonRedisSerializer`)
- Agent caches entities with JPA lazy fields — cache DTOs/response objects, not entities
- Agent uses no TTL — always set expiry, memory is not infinite
- Agent forgets `@EnableCaching` — `@Cacheable` silently does nothing without it
- Agent caches `null` values — use `.disableCachingNullValues()` to avoid storing misses
- Agent leaves hot keys unprotected — use `@Cacheable(sync = true)` to prevent stampede on expiry
- Agent gives every entry the s