Opinionated multi-level caching for Spring Boot that combines Redis for the distributed tier and Caffeine for the in-memory tier, guarded by a Resilience4j circuit breaker.
- Drop-in starter – activates automatically when
spring.cache.type=redis - Aggressive hot-path focus – randomized local TTL keeps Redis warm while preventing stampedes
- Graceful degradation – circuit breaker keeps serving from Caffeine if Redis is slow or down
- Batteries included – curated defaults so you only tweak what matters
<dependency>
<groupId>io.github.suppierk</groupId>
<artifactId>spring-boot-multilevel-cache-starter</artifactId>
<version>3.5.7.1</version>
</dependency>implementation 'io.github.suppierk:spring-boot-multilevel-cache-starter:3.5.7.1'examples/basic-demo— minimal REST service demonstrating@Cacheablewith the starter. Clone the repo, start Redis viadocker compose up -dinside the example directory, then run./gradlew :examples:basic-demo:bootRunfrom the project root.
- Microservices working with immutable cached entities under low latency requirements
- The goal is to not only reduce the number of calls to external service but also reduce the number of calls to Redis
- Mutable cached entities
- Entities with short time to live (< 5 minutes)
- Cases when entities in local cache must outlive entities in distributed cache
- Consider using only local cache instead
- Cases when all calls to Redis must be synchronized with distributed locks
- Use well-known Spring primitives for implementation
- Microservices environment needs to fit the requirement of fault tolerance:
- Redis calls covered by Resilience4j Circuit Breaker which allows falling back to use local cache at the cost of increased latency and more calls to external services.
- Redis TTL behaves similar to
expireAfterWritein Caffeine which allows us to set randomized expiry time for local cache:- This is useful to ensure that local cache entries will expire earlier for a higher chance to hit Redis instead of performing external call.
- This also implicitly reduces the load on the Redis by spreading calls to it over time.
- In the case of Redis connection errors, randomized expiry and Circuit Breaker will help to mitigate thundering herd problem.
- Expiry randomization follows the rule:
(time-to-live / 2) * (1 ± ((expiry-jitter / 100) * RNG(0, 1))), for example:- If
spring.cache.multilevel.time-to-liveis1h - And
spring.cache.multilevel.local.expiry-jitteris50(percents) - Then entries in local cache will expire in approximately
15-45m:
- If
(1h / 2) * (1 ± ((50 / 100) * RNG(0, 1))) ->
30m * (1 ± MAXRNG(0.5)) ->
30m * RANGE(0.5, 1.5) ->
15-45m
| Property | Default | Notes |
|---|---|---|
spring.cache.multilevel.time-to-live |
1h |
TTL applied to Redis entries; local cache derives its randomized expiry from here unless overridden |
spring.cache.multilevel.use-key-prefix |
false |
Enables key-prefix; set to true only when you supply a non-empty prefix |
spring.cache.multilevel.key-prefix |
"" |
Optional Redis key prefix |
spring.cache.multilevel.topic |
cache:multilevel:topic |
Redis Pub/Sub channel used to broadcast evictions |
spring.cache.multilevel.local.max-size |
2000 |
Maximum number of entries retained in Caffeine |
spring.cache.multilevel.local.expiry-jitter |
50 |
Percentage used to randomize the local TTL |
spring.cache.multilevel.local.expiration-mode |
after-create |
One of after-create, after-update, after-read |
spring.cache.multilevel.local.time-to-live |
empty | Optional dedicated TTL for the local cache |
spring.cache.multilevel.circuit-breaker.* |
see YAML | Passed directly to Resilience4j’s circuit breaker builder |
spring:
data:
redis:
host: ${HOST:localhost}
port: ${PORT:6379}
cache:
type: redis
# These properties are custom
multilevel:
# Redis properties
time-to-live: 1h
use-key-prefix: false
key-prefix: ""
topic: "cache:multilevel:topic"
# Local Caffeine cache properties
local:
max-size: 2000
expiry-jitter: 50
expiration-mode: after-create
# other valid values for expiration-mode: after-update, after-read
# Resilience4j Circuit Breaker properties for Redis
circuit-breaker:
failure-rate-threshold: 25
slow-call-rate-threshold: 25
slow-call-duration-threshold: 250ms
sliding-window-type: count_based
permitted-number-of-calls-in-half-open-state: 20
max-wait-duration-in-half-open-state: 5s
sliding-window-size: 40
minimum-number-of-calls: 10
wait-duration-in-open-state: 2500msPull requests are welcome. Before submitting, please run:
./gradlew spotlessApply check test
./gradlew jmhBenchmarks are kept short so you can verify regressions locally without burning an afternoon.