Skip to content

SuppieRK/spring-boot-multilevel-cache-starter

Spring Boot multi-level cache starter

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.

Highlights

  • 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

Build status Maven Central Javadoc SonarCloud Quality Gate SonarCloud Coverage SonarCloud Maintainability FOSSA Status

Usage

Maven

<dependency>
    <groupId>io.github.suppierk</groupId>
    <artifactId>spring-boot-multilevel-cache-starter</artifactId>
    <version>3.5.7.1</version>
</dependency>

Gradle

implementation 'io.github.suppierk:spring-boot-multilevel-cache-starter:3.5.7.1'

Examples

  • examples/basic-demo — minimal REST service demonstrating @Cacheable with the starter. Clone the repo, start Redis via docker compose up -d inside the example directory, then run ./gradlew :examples:basic-demo:bootRun from the project root.

Use cases

Suitable for

  • 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

Not a good fit for

  • 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

Ideas

  • 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 expireAfterWrite in 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-live is 1h
    • And spring.cache.multilevel.local.expiry-jitter is 50 (percents)
    • Then entries in local cache will expire in approximately 15-45m:
(1h / 2) * (1 ± ((50 / 100) * RNG(0, 1))) ->
30m * (1 ± MAXRNG(0.5)) ->
30m * RANGE(0.5, 1.5) ->
15-45m

Configuration options

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

Default configuration

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: 2500ms

Honorable mentions

Contributing

Pull requests are welcome. Before submitting, please run:

./gradlew spotlessApply check test
./gradlew jmh

Benchmarks are kept short so you can verify regressions locally without burning an afternoon.