A protection-enhancement annotation ecosystem for Spring Cache — beyond
@Cacheable, use a single @RedisCacheable annotation to add cache-penetration,
cache-breakdown, cache-avalanche, and hot-key early-refresh defenses to your
Redis cache. Protection is injected through a composable responsibility chain,
without re-inventing AOP.
Project status: early (v0.0.2) · Non-SLA best-effort · solo-maintained. Read
⚠️ Known Limitations before any production use.
Spring Cache (@Cacheable / @CachePut / @CacheEvict) solves "caching", not
"protection" — cache penetration, breakdown, avalanche, and hot-key expiry are
left to the business layer. ResiCache turns these defenses into declarative
capabilities via @RedisCacheable enhancement annotations and a
composable responsibility chain.
- Coexists with Spring Cache: extends
RedisCacheManager/CacheInterceptor— does not replace@EnableCaching, does not re-invent AOP. - Difference from JetCache: JetCache focuses on multi-level caching; ResiCache focuses on cache-defense-in-depth — every handler on the chain is pluggable and composable, which JetCache does not offer.
| Feature | Description |
|---|---|
| Bloom filter | Prevents cache penetration; blocks non-existent keys |
| Distributed lock | Redisson-based; prevents cache breakdown (requires Redisson on classpath) |
| TTL jitter | Randomizes TTL; prevents cache avalanche |
| Null-value caching | Caches null; prevents penetration |
| Early expiration | Async early refresh for hot keys; improves hit rate |
| Composable chain | Handlers strung together by priority; custom handlers can be inserted (differentiator) |
| Safe serialization | Whitelisted deserialization; defends against Jackson polymorphic-type attacks |
ResiCache does not provide circuit breaking / rate limiting / multi-level local cache / Reactive support — see Not in Scope.
ResiCache uses a responsibility chain for cache-write protection. Handler
ordering is defined in a single source of truth, the HandlerOrder enum, bound
via @HandlerPriority:
┌─────────────────────────────────────────────────────────────┐
│ CacheHandlerChain │
├─────────────────────────────────────────────────────────────┤
│ ① BloomFilter (100) ── Bloom filter, anti-penetration │
│ ② SyncLock (200) ── Distributed lock, anti-breakdown│
│ ③ EarlyExpiration (250) ── Early expiry, hot-key guard │
│ ④ TTL (300) ── TTL jitter, anti-avalanche │
│ ⑤ NullValue (400) ── Null caching, anti-penetration │
│ ⑥ ActualCache (500) ── Actual Redis write │
└─────────────────────────────────────────────────────────────┘
Any handler can set output.skipRemaining=true to short-circuit the rest of the
chain; PostProcessHandler callbacks run after the chain completes. Third-party
handlers can insert by extending the HandlerOrder enum.
<dependency>
<groupId>io.github.davidhlp</groupId>
<artifactId>ResiCache</artifactId>
<version>0.0.2</version>
</dependency>spring:
data:
redis:
host: localhost
port: 6379ResiCache activates via Spring Boot auto-configuration (entry point
RedisCacheAutoConfiguration, seeMETA-INF/spring/...AutoConfiguration.imports). No extra@EnableXxxis required.
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}Recommended: @RedisCacheable (the protection entry point)
@Service
public class UserService {
@RedisCacheable(value = "users", key = "#id",
useBloomFilter = true, // Bloom filter, anti-penetration
cacheNullValues = true, // null caching
randomTtl = true, // TTL jitter, anti-avalanche
variance = 0.2, // jitter amplitude ±20%
enableEarlyExpiration = true) // hot-key early refresh
public User getUserById(Long id) {
return userRepository.findById(id);
}
}Compatible: @Cacheable (no protection)
@Cacheable(value = "users", key = "#id") // coexists, but gains no protection
public User getUserById(Long id) { ... }
@Cacheablecoexists with ResiCache but gains no protection — the protection attributes (useBloomFilter/randomTtl/ ...) live only on@RedisCacheable. Since v0.0.3, the defaultnativeAnnotationMode=SELECTIVEmeans plain@Cacheableis handled entirely by Spring's native cache infrastructure. Use@RedisCacheablefor protection.
All properties use the resi-cache.* prefix (bound to RedisProCacheProperties).
resi-cache:
enabled: true # master kill-switch; false disables ResiCache entirely
protection:
enabled: true # false skips bloom/lock/early-exp/null-value; TTL preserved (startup-only)resi-cache:
default-ttl: 30m
key-prefix: ""
transaction-aware: false
fail-on-spel-error: trueresi-cache:
bloom-filter:
enabled: true
expected-insertions: 100000
false-probability: 0.01
hash-cache-size: 10000
rebuild-window-seconds: 30 # post-CLEAR rebuild window (s); 0 = disabled (v0.0.x behavior)resi-cache:
sync-lock:
timeout: 3000
unit: MILLISECONDS
prefix: "cache:lock:"
local-only: false # true = accept single-JVM sync when Redisson absent (else fail-fast)resi-cache:
early-expiration:
enabled: true
pool-size: 2
max-pool-size: 10
queue-capacity: 100resi-cache:
serializer:
type-property: "@class"
polymorphic-typing-enabled: false # off by default, safer
fail-on-unknown-type: true
allowed-package-prefixes: # deserialization whitelist
- "io.github.davidhlp."
- "com.example." # ← you MUST add your own business packages
⚠️ The whitelist defaults to onlyio.github.davidhlp.. When caching custom business types (e.g.com.example.User), you must add your package toallowed-package-prefixes, otherwise deserialization throws.Wildcard form (added in v0.0.3): any prefix ending in
.*is a wildcard sentinel — it matches the class directly (com.example.Foo), all sub-package classes (com.example.sub.Bar,com.example.foo.bar.baz.Qux, …), and is dot-boundary protected (socom.example.*does not matchcom.exampleX.Foo). Use it when you want to allow a whole package subtree without listing each sub-package.resi-cache: serializer: allowed-package-prefixes: - "io.github.davidhlp." - "com.example.*" # entire com.example subtree in one entry
| Attribute | Default | Description |
|---|---|---|
ttl |
60 | Cache TTL (seconds) |
cacheNullValues |
false | Cache null |
useBloomFilter |
false | Enable Bloom filter |
expectedInsertions |
10000 | Bloom expected insertions |
falseProbability |
0.03 | Bloom false-positive rate |
randomTtl |
false | Enable TTL jitter |
variance |
0.2 | TTL jitter amplitude |
enableEarlyExpiration |
false | Enable early expiry |
earlyExpirationThreshold |
0.3 | Early-expiry threshold (remaining TTL ratio) |
sync / syncTimeout |
false / 10 | Sync wait & timeout |
The five protection attributes default to
false— enable each explicitly on@RedisCacheable.sync=true(anti-breakdown) requires Redisson on the classpath; without it, ResiCache fails fast (refuses to silently degrade to a single-JVM lock, which is useless across instances). For an explicit single-instance/test degradation, setresi-cache.sync-lock.local-only=true.
Cache penetration — the Bloom filter intercepts requests for non-existent
keys before the cache layer. Cache breakdown — a distributed lock ensures
only one request loads the data. Cache avalanche — TTL randomization
(TTL = baseTtl ± variance × baseTtl when randomTtl=true) avoids mass
simultaneous expiry.
ResiCache is one of four common options for caching on top of Redis: JetCache,
Caffeine, raw Redisson, and ResiCache. The detailed feature matrix, honest
trade-offs, and "when to pick which" guidance live in
docs/comparison.md — including the line the project
ships under: "ResiCache for Redisson — the declarative cache protection
chain Redisson forgot to ship".
Headline takeaway: the 3 protections JetCache is missing, in one Redisson-native chain — bloom-filter (penetration), TTL jitter (avalanche), and distributed breakdown lock (breakdown). ResiCache is the Redisson companion that closes those gaps; JetCache is the multi-level / broadcast invalidator. The two are complementary in scope, not direct substitutes.
- Protection off by default: the five protection attributes default
false; enable each explicitly on@RedisCacheable. - Serialization envelope incompatible with Spring native: ResiCache uses a
{version, payload}envelope, incompatible with Spring'sGenericJackson2JsonRedisSerializer/JdkSerializer— existing projects must migrate, otherwise the entire cache misses on cutover. - Serialization whitelist defaults to the author's package:
allowed-package-prefixesdefaults toio.github.davidhlp.; custom types must be added explicitly (see Serialization safety). nativeAnnotationModedefaults toSELECTIVE: plain@Cacheableis handled entirely by Spring's native cache infrastructure, removing the dual-advisor risk. Use@RedisCacheablefor protection.- No Reactive support (WebFlux /
Mono/Flux):RedisCacheInterceptoris blocking; such methods log an explicit "caching will not take effect" warning. @CacheEvict(allEntries=true)(CLEAN) is best-effort, not atomic — parity with Spring's nativeRedisCache.clear/DefaultRedisCacheWriter.clean: it uses a SCAN cursor + batched UNLINK/DEL, so keys written mid-CLEAN may be stranded and the cache is briefly half-deleted on large key sets. Lua/MULTI atomicity is intentionally not used (Redis single-thread O(keyspace) block, Cluster cross-slot). When the Bloom filter is enabled,rebuild-window-secondsprevents silent nulls during the post-wipe rebuild.
ResiCache deliberately omits these to avoid bloat — pair with dedicated tools:
- Circuit breaking / rate limiting → Resilience4j
- Multi-level local + remote cache → Caffeine for the local tier
- Reactive caching (WebFlux) → not supported
| Dependency | Version |
|---|---|
| Spring Boot | 4.0.0 (parent) |
| Java | 21+ |
| Redisson | 3.50.0 (optional) |
| Caffeine | 3.1.8 |
| Testcontainers | 1.20.4 (CI Docker compatibility override) |
Full matrix: COMPATIBILITY.md.
- Version: v0.0.2 — Semantic Versioning < 1.0; APIs may change in minor
releases; breaking items are marked
⚠️ in CHANGELOG.md. - Maintenance: solo-maintained (DavidHLP), Non-SLA best-effort — no guaranteed response time, but issues are actively addressed.
- Contributing: PRs welcome — see CONTRIBUTING.md.
- Security: report privately — see SECURITY.md.
- Compatibility: see COMPATIBILITY.md.
MIT License © 2026 DavidHLP