Bluetape4k Cache Part 1: Why the Cache Module Looks This Way

This is Part 1 of the Bluetape4k Cache series. The full series continues with Part 1: cache module overview, Part 2: Near Cache, Part 3: Near Cache + Exposed strategies, and Part 4: workshop examples.
bluetape4k-cache starts from a familiar question: “can we just wrap a Map?” Production
usually makes that question bigger than it first looked.
Local caches are fast. Redis shares state. JCache gives us a standard API. Kotlin coroutine
code needs a suspend path. When a remote cache is briefly unstable, retry and fallback also
matter.
Wire those pieces by hand in every application, and cache code eventually takes more space than domain code. At that point, caching is no longer just a performance optimization. It is operational complexity.
That is why the Bluetape4k Cache module does not start as a loose collection of cache implementations. It first defines a shared language for cache behavior, then plugs provider-specific implementations into that language.

cache-core defines the common contract, and provider modules share the same usage model.The Big Picture
Section titled “The Big Picture”The starting point is cache-core: the layer that lets application code read, write,
invalidate, and handle failures in a similar shape regardless of provider.
| Area | Role |
|---|---|
| JCache helpers | JSR-107 style cache configuration and provider initialization |
SuspendCache | A suspend-friendly cache contract for coroutine code |
| Memoizer | Caches function results across sync, async, and suspend paths |
| NearCache interfaces | Describes an L1 local + L2 backing cache structure |
| Resilient decorator | Adds retry, fallback, and statistics outside the cache implementation |
Provider modules sit behind that contract and talk to the actual storage systems.
| Module | Role |
|---|---|
cache-core | Caffeine, Cache2k, Ehcache, JCache, and common NearCache APIs |
cache-lettuce | Redis/Lettuce JCache and RESP3 NearCache |
cache-redisson | Redisson JCache and RLocalCachedMap NearCache |
cache-hazelcast | Hazelcast JCache and IMap NearCache |
Full source: bluetape4k-projects/cache
Local Cache: Fast, but Alone
Section titled “Local Cache: Fast, but Alone”A local cache lives inside the JVM. There is no network round trip, so it is fast and simple to configure.
import io.bluetape4k.cache.caffeine.caffeineimport java.time.Duration
val cache = caffeine { maximumSize(10_000) expireAfterWrite(Duration.ofMinutes(10))}.build<String, Any>()Full source: cache-core README
The limitation is just as clear. A local cache only knows its own JVM. If there are two application instances, there are two drawers. Assuming one drawer knows what the other drawer contains is how incident reports become a literary genre.
SuspendCache: A Contract That Fits Coroutine Code
Section titled “SuspendCache: A Contract That Fits Coroutine Code”In Kotlin services, a cache miss often reads from a database or calls a remote API. If that
path is suspend, the cache API should be suspend too.
import io.bluetape4k.cache.jcache.CaffeineSuspendJCache
val cache = CaffeineSuspendJCache<String, String>("sessions")
cache.put("token:1", "user-1")val userId = cache.get("token:1")Related source: CaffeineSuspendJCache.kt
Memoizer: Turning Function Results into Cache Entries
Section titled “Memoizer: Turning Function Results into Cache Entries”A memoizer lifts this property into the API: “this function often returns the same result for the same input.” Callers keep using a function, while the cache handles repeated work.
import io.bluetape4k.cache.memoizer.suspendMemoizer
val userSummary = suspendMemoizer<Long, String> { userId -> loadUserSummaryFromDatabase(userId)}
val summary = userSummary(42L)Related source: Memoizer.kt, LettuceMemoizer.kt
NearCache: A Fast Drawer and a Shared Warehouse
Section titled “NearCache: A Fast Drawer and a Shared Warehouse”NearCache combines an L1 local cache with an L2 backing cache. Frequently read values stay near the JVM, while shared state remains in the remote cache.
import io.bluetape4k.cache.LettuceCaches
val cache = LettuceCaches.nearCache<String>(redisClient) { cacheName = "users" maxLocalSize = 10_000 recordStats = true}
cache.put("42", "Debop")val name = cache.get("42")NearCacheOperations provides common operations such as get, put, remove, clearLocal,
clearAll, and stats. Whether the provider is Lettuce, Redisson, or Hazelcast, application
code keeps the same shape.
Related source: NearCacheOperations.kt, LettuceCaches.kt
Resilience Is Not Decoration
Section titled “Resilience Is Not Decoration”A cache looks like a performance feature. The moment a remote cache is involved, it also becomes a failure propagation path. If every request is affected when Redis is briefly unstable, the cache did more than make things slower. It became a failure amplifier.
Bluetape4k provides a resilience decorator for that reason.
val resilient = cache.withResilience { retryMaxAttempts = 5 retryWaitDuration = Duration.ofMillis(200) getFailureStrategy = GetFailureStrategy.RETURN_FRONT_OR_NULL}Related source: ResilientNearCacheDecorator.kt, NearCacheResilienceConfig.kt
Wrap-up
Section titled “Wrap-up”The Cache module is trying to keep one problem simple.
- Use local caches for speed.
- Use remote caches for shared state.
- Match suspend APIs to coroutine code.
- Use NearCache to connect the two worlds.
- Use resilience so a brief “hold on” does not become a full outage.
Next, we will pull NearCache out on its own: why remote cache alone is often not enough, how Redisson Pub/Sub differs from Lettuce RESP3 CLIENT TRACKING, and what the benchmark numbers say. Numbers can be blunt. That is part of their charm.
Comments
Leave a note or reaction with your GitHub account.