mcpbeat

Spring Data Redis

rrezartprebreza/spring-boot-spring-data-redis

> Use when implementing caching, session storage, rate limiting, or any Redis integration. Covers cache-aside pattern, key naming, TTL strategy, and serialization config.

3k tokens
context cost
the whole folder, loaded on every use
4
files
instructions only
0
copies elsewhere
how many repositories repackaged it
190
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill spring-data-redis

The instruction itself

10 sections, as written by the author

Spring Data Redis

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>

Configuration

@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();
    }
}

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

@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

@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);
    }
}

Rate Limiting with Redis

@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

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:

// 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 (GenericJacksonJsonRedisSerializer)
  • 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 same TTL — add jitter so keys don't expire in a synchronized wave
  • Agent uses GenericJackson2JsonRedisSerializer — deprecated Jackson 2 API; use GenericJacksonJsonRedisSerializer (Jackson 3, tools.jackson)
  • Agent expects default typing out of the box — the Jackson 3 serializer ships with it OFF; call .enableDefaultTyping(validator) or @Cacheable hits come back as LinkedHashMap and throw ClassCastException
  • Agent registers JavaTimeModule on the mapper — Jackson 3 handles java.time natively; no module needed
  • Agent declares a generic ObjectMapper bean to customize JSON — declare a JsonMapper bean or a JsonMapperBuilderCustomizer instead
  • Migrating from Boot 3: Spring Session keys moved spring.session.redis.*spring.session.data.redis.*

How to use it

Copy the folder

Take rrezartprebreza/spring-boot-spring-data-redis from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.