Tiercache is a family of JVM libraries providing a correct-out-of-the-box two-level cache: L1 in-process (Caffeine) + L2 Redis/Valkey, with cross-instance invalidation, built-in protection against the classic high-load failure modes (stampede, avalanche, penetration, L2 degradation), and first-class observability. A framework-independent core with thin adapters — pull in exactly one starter module and keep your existing cache code.
The cache is eventually consistent by design; no strong-consistency guarantees are given or implied.
- Two-level read cascade — L1 (shaded Caffeine, zero-allocation hit path) → L2 (Redis/Valkey) → your loader. An L2 hit always warms L1, so the next read of the same key is served in-process.
- Correct by default — singleflight per instance plus cluster-wide rebuild coordination (distributed lock with watchdog lease extension and mandatory double-check), TTL jitter, fail-fast TTL-ordering validation, atomic
putIfAbsent— all on without configuration; disabling requires an explicit opt-in and is logged as a risk. Null caching is opt-in (null-policy: allow) with a tri-statelookupto distinguish miss from cached-null. - Cross-instance invalidation — versioned events with last-write-wins ordering, a bounded journal with replay on reconnect, and two transport profiles: lightweight Pub/Sub or durable Redis Streams. Pub/Sub uses bounded per-cache delivery queues with explicit recovery after overflow.
- Honest degradation — a circuit breaker switches protected cache operations to local fallback when Redis fails. Loader errors, invalid configuration and async executor rejection remain visible. Recovery replays recorded invalidations when journal history is readable and intact. Missing or unverifiable history, a replay-read failure, or a missing journal can trigger a full L1 flush and a source-load burst. Writes that never reached Redis cannot be reconstructed by replay; see recovery semantics for the fallback signals and limits.
- Stale serving — stale-while-revalidate and XFetch early refresh keep hot keys fast while values refresh in the background.
- Observability as a feature — Micrometer metrics for cache outcomes, degradation and invalidation, OpenTelemetry tracing, JMX inspection, and a reference Grafana dashboard with alert rules in
docs/grafana/. Some cleanup failures are log-only; see the observability catalog. - Kotlin coroutines —
suspendAPI, invalidationFlow, and atierCache { }config DSL intiercache-kotlin. A suspending loader runs on the caller's coroutine dispatcher, so a blocking loader blocks that dispatcher — offload blocking work withwithContext(Dispatchers.IO). - Bounded async executor — async/Reactor/coroutines operations run on a bounded, library-managed pool (size via
tiercache.async-executor-threadsorTierCacheFactory.Builder.asyncExecutorThreads); coalesced followers release workers, while load owners remain blocking. A shared retained-resource limit also bounds detached waiters; saturation fails the returned stage withRejectedExecutionException. See async resource ownership. - GraalVM Native Image — reachability metadata ships inside the published jars; the demo application compiles natively in CI.
// build.gradle.kts
implementation("io.github.cramen:tiercache-spring-boot-starter:2.1.0")# application.yml
tiercache:
enabled: true
redis-uri: redis://localhost:6379The starter replaces the standard cache manager: @Cacheable / @CachePut / @CacheEvict code works unchanged, backed by the two-level cache. Load coalescing under @Cacheable requires sync = true — Spring's abstraction only coordinates concurrent loads in sync mode (see migration from Spring Cache). Null caching is opt-in (tiercache.caches.<name>.null-policy: allow). Per-cache overrides live under tiercache.caches.<name>.*; invalid configuration (e.g. L1 TTL > L2 TTL) aborts startup with an actionable error. A runnable demo lives in examples/demo-spring (docker-compose included).
// build.gradle.kts
implementation("io.github.cramen:tiercache-micronaut:2.1.0")# application.yml
tiercache:
enabled: true
redis-uri: redis://localhost:6379The module replaces Micronaut's DefaultCacheManager: @Cacheable / @CachePut / @CacheInvalidate code works unchanged, backed by the two-level cache. The tiercache.* property keys and defaults are identical to the Spring Boot starter's, and per-cache overrides live under tiercache.caches.<name>.* — docs/configuration.md is the single configuration reference. The Pub/Sub invalidation profile is wired by default; each named cache gets its own L2 namespace (micronaut:<cache-name>).
// build.gradle.kts
implementation("io.github.cramen:tiercache-core:2.1.0")
implementation("io.github.cramen:tiercache-transport-redis:2.1.0")import io.tiercache.TierCache;
import io.tiercache.TierCacheFactory;
import io.tiercache.redis.LettuceRemoteCache;
try (LettuceRemoteCache<String, String> l2 = LettuceRemoteCache
.<String, String>builder("redis://localhost:6379")
.cacheName("users")
.build();
TierCacheFactory factory = TierCacheFactory.builder()
.remoteCache(l2)
.build()) {
TierCache<String, String> cache = factory.getCache("users");
String name = cache.getOrCompute("user:42", key -> loadFromDatabase(key));
}Reads cascade L1 → L2 → loader; concurrent loads of the same key share one loader execution, per instance and cluster-wide. All protections (singleflight, rebuild coordination, TTL jitter, circuit breaker) are active in this snippet with no extra configuration.
| Module | What it gives you |
|---|---|
tiercache-spring-boot-starter |
Spring Boot 3.5 / 4.1 consumer-tested auto-configuration — the one dependency most Spring apps need |
tiercache-micronaut |
Micronaut CacheManager/SyncCache/AsyncCache adapter — the one dependency Micronaut apps need |
tiercache-core |
The framework-independent cache engine: cascade, singleflight, rebuild coordination, degradation |
tiercache-transport-redis |
Lettuce-backed Redis/Valkey L2 and invalidation transport |
tiercache-kotlin |
Coroutines API: suspend facade, invalidation Flow, config DSL |
tiercache-micrometer |
Micrometer metrics, OpenTelemetry tracing, JMX inspection |
tiercache-invalidation |
Invalidation protocol internals (pulled in transitively by the transport) |
tiercache-tck |
Public chaos-test suite and benchmarks |
- Java 17 or newer
- Redis 6.2+ or Valkey (for L2 and cross-instance features). See the tested platform matrix for exact versions and Sentinel scope; Redis Cluster is unsupported by the stock transport.
- Docker, to run the integration tests and TCK chaos suite locally
TierCache 2.0 introduces Redis keyspace v2, a breaking operational change. It requires a coordinated cold-cache cutover; old/new instances are not rolling-compatible. See the v2 migration guide before upgrading from 1.x.
All published Maven artifacts follow semantic versioning: patch releases for backwards-compatible fixes, minor releases for backwards-compatible additions, major releases for breaking changes.
Supported public API — the documented entry points only:
io.tiercachecore API:TierCache,AsyncTierCache,TierCacheFactory,CacheSettings,CacheOverride- Spring Boot starter properties (
tiercache.*) and annotations - Micronaut
tiercache.*properties (tiercache-micronaut) - Kotlin
suspend/Flowextensions intiercache-kotlin - Reactor
Mono/Fluxfacade intiercache-reactor
Everything else — builders, transport internals, metrics helpers, and any type not listed above — is internal and may change in any release without notice. Minor and patch upgrades never break consumers who use only the supported API.
Deprecation and removal — before any public API element is removed, it is marked @Deprecated with a documented replacement and stays functional for at least one minor release. Removals are recorded in CHANGELOG.md and UPGRADING.md, which describes the migration path per release.
- Strong-consistency requirements. Tiercache is eventually consistent by design: L1 copies on other instances lag writes by a bounded staleness window. If your use case needs read-your-writes across instances, use the database or a strongly consistent store directly.
- Single-instance deployments. With one node there is nothing to invalidate and nothing to coordinate — L2 plus the invalidation protocol is pure overhead. Plain Caffeine is the better fit.
- Tiny working sets. If your hot data fits comfortably in an in-process cache, an L2 round trip on every cold read costs more than it saves; L2 pays off when L1 misses are frequent or expensive.
- Write-mostly workloads. A cache amortizes reads over writes. If writes dominate, invalidation churn and L2 write traffic add latency without the read hit rate to justify it.
Start with the documentation index.
- Configuration reference
- Migration from Spring Cache
- Migration from Redisson
- Migration from JetCache
- Troubleshooting
- Verifying release artifacts
- Sizing and TTL guidance
- Observability: metrics, tracing, dashboards
- Recovery and fallback semantics
- Running the TCK chaos suite
- Grafana dashboard and alert rules
- Security policy and supply-chain verification
- Demo application
Maintainer build, validation and release procedures are collected in maintenance.