Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when implementing caching, session storage, rate limiting, or any Redis integration. Covers cache-aside pattern, key naming, TTL strategy, and serialization config.
.claude/skills/rrezartprebreza-spring-data-redis/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 22% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 22% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 12% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 35% | 0% |
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>
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(jsonSerializer()); // JSON, not Java serialize template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(jsonSerializer()); 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(jsonSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .withCacheConfiguration("orders", config.entryTtl(Duration.ofMinutes(5))) .withCacheConfiguration("products", config.entryTtl(Duration.ofHours(1))) .build(); } // Jackson 3 (tools.jackson) serializer. Default typing is OFF by default — // enable it (scoped to trusted packages) so cached Objects round-trip to their // real type instead of coming back as LinkedHashMap. private GenericJacksonJsonRedisSerializer jsonSerializer() { return GenericJacksonJsonRedisSerializer.builder() .enableDefaultTyping(BasicPolymorphicTypeValidator.builder() .allowIfSubType("com.example.") // your DTO packages .allowIfSubType("java.util.") // collections .build()) .build(); } }
{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.1java@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() {} }
java@Service @RequiredArgsConstructor public class OrderCacheService { private final RedisTemplate<String, Object> redisTemplate; private final JsonMapper jsonMapper; // Jackson 3 — Boot auto-configures a JsonMapper bean 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(jsonMapper.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); } }
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; } }
yamlspring: 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
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.
GenericJacksonJsonRedisSerializer)@EnableCaching — @Cacheable silently does nothing without itnull values — use .disableCachingNullValues() to avoid storing misses@Cacheable(sync = true) to prevent stampede on expiryGenericJackson2JsonRedisSerializer — deprecated Jackson 2 API; use GenericJacksonJsonRedisSerializer (Jackson 3, tools.jackson).enableDefaultTyping(validator) or @Cacheable hits come back as LinkedHashMap and throw ClassCastExceptionJavaTimeModule on the mapper — Jackson 3 handles java.time natively; no module neededObjectMapper bean to customize JSON — declare a JsonMapper bean or a JsonMapperBuilderCustomizer insteadspring.session.redis.* → spring.session.data.redis.*| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-19 | pass→pass | 19,727 | 8,924 | -55% | 1 | 1 | 0% | 3,256 | 3,571 | +10% | 0 | 0 | — |
case-01 | fail→pass | 22,595 | 16,432 | -27% | 1 | 1 | 0% | 3,659 | 4,466 | +22% | 0 | 0 | — |
case-02 | fail→pass | 15,621 | 14,847 | -5% | 1 | 1 | 0% | 3,216 | 3,913 | +22% | 0 | 0 | — |
case-03 | fail→pass | 33,716 | 19,885 | -41% | 1 | 1 | 0% | 4,687 | 5,244 | +12% | 0 | 0 | — |
case-04 | pass→pass | 20,342 | 20,589 | +1% | 1 | 1 | 0% | 3,082 | 5,260 | +71% | 0 | 0 | — |
case-05 | pass→pass | 12,201 | 14,098 | +16% | 1 | 1 | 0% | 2,428 | 3,702 | +52% | 0 | 0 | — |
case-06 | pass→pass | 18,225 | 15,697 | -14% | 1 | 1 | 0% | 2,556 | 5,242 | +105% | 0 | 0 | — |
case-07 | pass→pass | 15,651 | 12,158 | -22% | 1 | 1 | 0% | 2,742 | 3,225 | +18% | 0 | 0 | — |
case-08 | pass→pass | 9,875 | 8,116 | -18% | 1 | 1 | 0% | 1,649 | 3,063 | +86% | 0 | 0 | — |
case-09 | fail→pass | 13,955 | 3,505 | -75% | 1 | 1 | 0% | 1,555 | 2,471 | +59% | 0 | 0 | — |
case-10 | pass→pass | 20,221 | 21,349 | +6% | 1 | 1 | 0% | 3,335 | 4,425 | +33% | 0 | 0 | — |
case-11 | fail→pass | 13,665 | 8,116 | -41% | 1 | 1 | 0% | 2,235 | 3,028 | +35% | 0 | 0 | — |
case-12 | fail→fail | 16,124 | 5,963 | -63% | 1 | 1 | 0% | 2,371 | 2,858 | +21% | 0 | 0 | — |
case-13 | pass→pass | 11,236 | 4,381 | -61% | 1 | 1 | 0% | 1,132 | 2,617 | +131% | 0 | 0 | — |
case-14 | pass→pass | 11,271 | 7,843 | -30% | 1 | 1 | 0% | 2,168 | 3,495 | +61% | 0 | 0 | — |
case-15 | pass→pass | 5,414 | 3,525 | -35% | 1 | 1 | 0% | 1,066 | 2,501 | +135% | 0 | 0 | — |
case-16 | pass→pass | 4,326 | 3,902 | -10% | 1 | 1 | 0% | 941 | 2,393 | +154% | 0 | 0 | — |
case-17 | pass→pass | 14,943 | 12,236 | -18% | 1 | 1 | 0% | 2,325 | 3,506 | +51% | 0 | 0 | — |
case-18 | pass→pass | 10,132 | 9,762 | -4% | 1 | 1 | 0% | 1,679 | 3,271 | +95% | 0 | 0 | — |
case-20 | pass→pass | 9,039 | 9,132 | +1% | 1 | 1 | 0% | 1,840 | 3,341 | +82% | 0 | 0 | — |
case-21 | fail→pass | 19,664 | 26,288 | +34% | 1 | 1 | 0% | 3,402 | 5,808 | +71% | 0 | 0 | — |
case-22 | pass→pass | 20,463 | 16,613 | -19% | 1 | 1 | 0% | 2,882 | 4,925 | +71% | 0 | 0 | — |
case-23 | fail→pass | 15,033 | 9,019 | -40% | 1 | 1 | 0% | 2,880 | 3,540 | +23% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +30 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.