spring-data-redis
Skill344 starsChanged 3 months agoMITView on GitHub
Use when implementing caching, session storage, rate limiting, or any Redis integration. Covers cache-aside pattern, key naming, TTL strategy, and serialization config.
In this repository77 other files it gives its agents.
- agent-qa-browser-regressionSkill
- ai-observabilitySkill
- api-versioningSkill
- configuration-propertiesSkill
- container-native-deploymentSkill
- domain-driven-designSkill
- event-driven-messagingSkill
- flyway-migrationsSkill
- hateoasSkill
- hexagonal-architectureSkill
- http-interface-clientsSkill
- idempotency-patternsSkill
- java-supply-chain-securitySkill
- layered-architectureSkill
- mcp-serverSkill
- multi-module-mavenSkill
- multi-tenancySkill
- null-safetySkill
- oauth2-resource-serverSkill
- openapi-firstSkill
- problem-details-rfc9457Skill
- production-observabilitySkill
- resilience-retrySkill
- rest-api-conventionsSkill
- security-audit-loggingSkill
- spring-ai-integrationSkill
- spring-ai-securitySkill
- spring-batchSkill
- spring-boot-migrationSkill
- spring-cloud-gatewaySkill
- spring-data-jpaSkill
- spring-incident-responseSkill
- spring-modulithSkill
- spring-security-jwtSkill
- spring-security-reviewSkill
- testing-pyramidSkill
- transactional-patternsSkill
- webflux-reactive-patternsSkill
- agent-qa-browser-regressionSkill
- ai-observabilitySkill
- api-versioningSkill
- configuration-propertiesSkill
- container-native-deploymentSkill
- domain-driven-designSkill
- event-driven-messagingSkill
- flyway-migrationsSkill
- hateoasSkill
- hexagonal-architectureSkill
- http-interface-clientsSkill
- idempotency-patternsSkill
- java-supply-chain-securitySkill
- layered-architectureSkill
- mcp-serverSkill
- multi-module-mavenSkill
- multi-tenancySkill
- null-safetySkill
- oauth2-resource-serverSkill
- openapi-firstSkill
- problem-details-rfc9457Skill
- production-observabilitySkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 209
Add this bean method to the shown RedisConfig, reusing its jsonSerializer() helper, and mark the normal cacheManager bean @Primary so …
- Very specific: line 230
Use sync loading only for committed, read-only data.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 serialization
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)
.transactionAware() // put/evict only after the surrounding transaction commits
.withCacheConfiguration("orders", config.entryTtl(Duration.ofMinutes(5)))
.withCacheConfiguration("products", config.entryTtl(Duration.ofHours(1)))
.build();
}
// Records are final, so DefaultTyping.NON_FINAL writes no type id for them and every
// cache read fails ("missing type id property '@class'"). The builder's defaultTyping(true)
// types user classes too; the validator limits type ids to an allow-list instead of
// LaissezFaire's "any class". BigDecimal is non-final, so java.math must be allowed.
private GenericJackson2JsonRedisSerializer jsonSerializer() {
ObjectMapper mapper = JsonMapper.builder()
.addModule(new JavaTimeModule())
.polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.example.") // your DTO packages
.allowIfSubType("java.util.")
.allowIfSubType("java.math.")
.allowIfSubType("java.time.")
.build())
.build();
return GenericJackson2JsonRedisSerializer.builder()
.objectMapper(mapper)
.defaultTyping(true)
.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 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
@Component
@RequiredArgsConstructor
public class RateLimiter {
private final RedisTemplate<String, String> redisTemplate;
// INCR and PEXPIRE in ONE atomic script: as two separate commands, a crash or
// timeout between them leaves a key with no TTL and locks the caller out forever.
private static final RedisScript<Long> INCR_WITH_TTL = RedisScript.of(
"local c = redis.call('INCR', KEYS[1]) " +
"if c == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[1]) end " +
"return c", Long.class);
public boolean isAllowed(String identifier, int maxRequests, Duration window) {
String key = "ratelimit:" + identifier;
Long count = redisTemplate.execute(INCR_WITH_TTL, List.of(key), String.valueOf(window.toMillis()));
return count != null && 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: redisCache Stampede
sync = true delegates loading coordination to the cache provider. The default Redis writer
in the configuration above is non-locking: do not claim that this annotation alone prevents
duplicate loads, even inside one application instance.
For expensive read-only loads, the optional
CoalescingRedisCacheManager uses a locking
writer with a finite lock TTL. Register it under a distinct bean name, keeping your normal
cache manager primary, and pass a JSON-serialized RedisCacheConfiguration:
@Cacheable(cacheManager = "coalescingCacheManager", value = "products", key = "#id", sync = true)
public ProductResponse findById(UUID id) { ... }Add this bean method to the shown RedisConfig, reusing its jsonSerializer() helper,
and mark the normal cacheManager bean @Primary so existing annotations stay unchanged:
@Bean("coalescingCacheManager")
RedisCacheManager coalescingCacheManager(RedisConnectionFactory factory) {
var config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(5))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer()));
return CoalescingRedisCacheManager.create(factory, config, Duration.ofSeconds(10));
}The writer lock is cache-wide, not per key: unrelated misses may block too. Configure finite loader/transport timeouts and a lock TTL longer than the expected load, and test contention. Lock expiry during a stalled load can still permit duplicates; this is load reduction, not an exactly-once guarantee or protection for payment/order mutations. Redis command timeouts do not bound the writer's entire lock-wait loop. Use a bounded caller deadline or a different coalescing strategy when strict waiting bounds are required.
Use sync loading only for committed, read-only data; transaction-aware put/evict does not make
the loader-based cache get transactional. Prefer TTL jitter or local coalescing when the
cache-wide lock would reduce throughput excessively. Do not implement a bare SETNX/delete
lock without ownership tokens, atomic owner-checked release and a lease-expiry design.
See Spring Redis cache behavior.
Failure policy
| Usage | Redis unavailable | Recovery concern |
| Disposable read cache | Optionally fall back to the source, with bounded concurrency | Avoid a database overload during an outage |
| Write-side eviction | Do not silently ignore failures | A committed DB update can leave stale cached values; use bounded TTL plus durable invalidation when needed |
| Authentication/session/authorization | Do not grant access because Redis failed | Return a controlled unavailable response or require reauthentication |
| Security rate limit | Fail closed or use an explicitly approved bounded fallback | Never silently allow unlimited traffic |
OptionalCacheLookup demonstrates narrow read-side
fallback for connection failures and timeouts, with an observability callback. It deliberately
does not suppress serialization errors or write/eviction failures. Recovery restores cache
reads; do not assume transactionAware() makes Redis and the database one atomic transaction.
Verification must include concurrent misses, TTL expiration, DTO round-trips (including money/time fields), rollback, outage fallback and recovery. A short negative-cache TTL can be useful for repeated misses; disabling null caching is an example choice, not a universal rule.
Verification scope
The repository fixture targets Boot 3.5.16 with Java 17/21 and checks Redis 7.4 serialization, TTL, normal concurrent loads and transaction callbacks; not failover or lease-expiry correctness. Check the coverage matrix before treating another minor version or an illustrative snippet as verified.
Gotchas
- Agent combines
@Transactionalwith@CachePut/@CacheEvicton a non-transaction-aware cache manager — the cache can be updated before commit (or evicted then re-filled with stale data); build it with.transactionAware() - Agent implements a rate limiter as separate
INCRthenEXPIREcalls — a failure between them leaves a TTL-less key that blocks the caller forever; use one Lua script - Agent allow-lists only
java.util.for default typing —BigDecimalis non-final and carries a type id, so cached prices/totals fail to deserialize; allowjava.math.(andjava.time.) too - Agent uses Java serialization for values — always use JSON (
GenericJackson2JsonRedisSerializer) - Agent uses
new GenericJackson2JsonRedisSerializer()— its plain mapper has no JSR-310 module, so anyInstant/LocalDatefield fails to serialize; build it withJavaTimeModule - 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—@Cacheablesilently does nothing without it - Agent caches misses without an expiry policy - either disable null caching or use an intentional short negative-cache TTL.
- Agent assumes
sync = trueguarantees a single load - verify the provider/writer and test concurrent misses. - Agent gives every entry the same TTL — add jitter so keys don't expire in a synchronized wave
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

