CoCache Development Guide
CoCache is a Java/Kotlin two-level distributed coherent cache framework:
- L2 client-side cache: local Map, Guava, or Caffeine cache.
- L1 distributed cache: Redis-backed shared cache.
- Coherence:
CacheEvictedEventBus publishes evictions so peer instances invalidate local entries.
Start Here
Choose the smallest reference that fits the request:
| Task |
Read |
| Add CoCache to a Spring or Spring Boot app; configure Redis failure behavior or the missing-guard sentinel |
references/setup.md |
Compose cached values with @JoinCacheable |
references/join-cache.md |
| Write or update tests |
references/testing.md |
| Implement a custom L1/L2 cache, event bus, key converter, or source |
references/custom-implementation.md |
Repository Rules
When editing this repository, follow AGENTS.md:
- Use
me.ahoo.test.asserts.assert and .assert() in Kotlin tests; do not use AssertJ assertThat().
- Extend the TCK specs in
cocache-test for cache implementations.
- Ask before changing
cocache-api public interfaces or adding dependencies.
- Run the relevant Gradle checks before finishing; prefer
./gradlew check for broad changes.
Core Model
Define one cache interface per cache domain. The interface extends Cache<K, V>, is annotated with @CoCache, and is registered through @EnableCoCache. CoCache creates a proxy at runtime.
@CoCache(keyPrefix = "user:", ttl = 120)
@GuavaCache(maximumSize = 1_000_000, expireAfterAccess = 120, expireUnit = TimeUnit.SECONDS)
interface UserCache : Cache<String, User>
@SpringBootApplication
@EnableCoCache(caches = [UserCache::class])
class App
Use caches through Kotlin operators: cache[key], cache[key] = value, cache.evict(key), and cache.getCache(key) when CacheValue metadata is required.
Extension Points
CoCache auto-configures defaults, but each component can be overridden:
- Per cache, define named beans such as
UserCache.CacheSource, UserCache.ClientSideCache, UserCache.KeyConverter, or UserCache.JoinKeyExtractor.
- Globally, define beans by type; auto-configured beans use
@ConditionalOnMissingBean.
- For data loading, implement
CacheSource<K, V>.loadCacheValue(key) and return DefaultCacheValue.forever(value), DefaultCacheValue.ttlAt(value, ttl), or bounded DefaultCacheValue.missingGuard(ttl, amplitude) values. Returning null for a missing key is also valid — CoCache auto-caches a missing guard with the cache's TTL (cache-penetration protection).
Testing Pattern
CoCache provides abstract specs in cocache-test for compatibility coverage:
CacheSpec<K,V> for base cache behavior.
ClientSideCacheSpec<V> for L2 caches.
DistributedCacheSpec<V> for L1 caches.
DefaultCoherentCacheSpec<K,V> for two-level coherent cache behavior and cache breakdown protection.
MultipleInstanceSyncSpec<K,V> and CacheEvictedEventBusSpec for cross-instance coherence and event buses.
Use the repo's createCacheEntry(): Pair<K, V> contract:
class MyDistributedCacheTest : DistributedCacheSpec<String>() {
override fun createCache(): DistributedCache<String> {
return MyDistributedCache()
}
override fun createCacheEntry(): Pair<String, String> {
return UUID.randomUUID().toString() to "test_value"
}
}
Key Classes
| Class |
Module |
Purpose |
Cache<K,V> |
cocache-api |
Base cache interface |
CoherentCache<K,V> |
cocache-core |
Two-level cache engine |
DefaultCoherentCache |
cocache-core |
Default implementation |
ClientSideCache<V> |
cocache-api |
L2 local cache interface |
MapClientSideCache |
cocache-core |
ConcurrentHashMap impl |
GuavaClientSideCache |
cocache-core |
Guava Cache impl |
CaffeineClientSideCache |
cocache-core |
Caffeine Cache impl |
DistributedCache<V> |
cocache-core |
L1 distributed cache interface |
RedisDistributedCache |
cocache-spring-redis |
Redis impl |
CacheSource<K,V> |
cocache-api |
Data source loader |
CacheEvictedEventBus |
cocache-core |
Event bus for coherence |
RedisCacheEvictedEventBus |
cocache-spring-redis |
Redis Pub/Sub impl |
JoinCache<K1,V1,K2,V2> |
cocache-api |
Composed cache interface |
SimpleJoinCache |
cocache-core |
Default JoinCache impl |
KeyFilter |
cocache-core |
Bloom filter for cache breakdown protection |
BloomKeyFilter |
cocache-core |
Guava BloomFilter impl |
Build Commands
./gradlew build -x test
./gradlew test
./gradlew :cocache-core:test
./gradlew :cocache-core:test --tests "me.ahoo.cache.proxy.ProxyCacheTest"
./gradlew :cocache-spring-redis:check
./gradlew :cocache-spring-boot-starter:check
./gradlew check
1---2name: cocache3description: Use when building or modifying Java/Kotlin applications with CoCache two-level distributed coherent caching. Invoke for @CoCache cache interfaces, @JoinCacheable composition, Redis-backed coherence, Spring Boot integration, cache proxy behavior, custom cache backends, cache penetration protection (missing guards), cache breakdown protection, Redis failure policy (strict-failure, missing-guard sentinel), Redis TTL-drift test failures, or CoCache TCK tests.4---56# CoCache Development Guide78CoCache is a Java/Kotlin two-level distributed coherent cache framework:9- L2 client-side cache: local Map, Guava, or Caffeine cache.10- L1 distributed cache: Redis-backed shared cache.11- Coherence: `CacheEvictedEventBus` publishes evictions so peer instances invalidate local entries.1213## Start Here1415Choose the smallest reference that fits the request:1617| Task | Read |18|------|------|19| Add CoCache to a Spring or Spring Boot app; configure Redis failure behavior or the missing-guard sentinel | `references/setup.md` |20| Compose cached values with `@JoinCacheable` | `references/join-cache.md` |21| Write or update tests | `references/testing.md` |22| Implement a custom L1/L2 cache, event bus, key converter, or source | `references/custom-implementation.md` |2324## Repository Rules2526When editing this repository, follow `AGENTS.md`:27- Use `me.ahoo.test.asserts.assert` and `.assert()` in Kotlin tests; do not use AssertJ `assertThat()`.28- Extend the TCK specs in `cocache-test` for cache implementations.29- Ask before changing `cocache-api` public interfaces or adding dependencies.30- Run the relevant Gradle checks before finishing; prefer `./gradlew check` for broad changes.3132## Core Model3334Define one cache interface per cache domain. The interface extends `Cache<K, V>`, is annotated with `@CoCache`, and is registered through `@EnableCoCache`. CoCache creates a proxy at runtime.3536```kotlin37@CoCache(keyPrefix = "user:", ttl = 120)38@GuavaCache(maximumSize = 1_000_000, expireAfterAccess = 120, expireUnit = TimeUnit.SECONDS)39interface UserCache : Cache<String, User>4041@SpringBootApplication42@EnableCoCache(caches = [UserCache::class])43class App44```4546Use caches through Kotlin operators: `cache[key]`, `cache[key] = value`, `cache.evict(key)`, and `cache.getCache(key)` when `CacheValue` metadata is required.4748## Extension Points4950CoCache auto-configures defaults, but each component can be overridden:51- Per cache, define named beans such as `UserCache.CacheSource`, `UserCache.ClientSideCache`, `UserCache.KeyConverter`, or `UserCache.JoinKeyExtractor`.52- Globally, define beans by type; auto-configured beans use `@ConditionalOnMissingBean`.53- For data loading, implement `CacheSource<K, V>.loadCacheValue(key)` and return `DefaultCacheValue.forever(value)`, `DefaultCacheValue.ttlAt(value, ttl)`, or bounded `DefaultCacheValue.missingGuard(ttl, amplitude)` values. Returning `null` for a missing key is also valid — CoCache auto-caches a missing guard with the cache's TTL (cache-penetration protection).5455## Testing Pattern5657CoCache provides abstract specs in `cocache-test` for compatibility coverage:58- `CacheSpec<K,V>` for base cache behavior.59- `ClientSideCacheSpec<V>` for L2 caches.60- `DistributedCacheSpec<V>` for L1 caches.61- `DefaultCoherentCacheSpec<K,V>` for two-level coherent cache behavior and cache breakdown protection.62- `MultipleInstanceSyncSpec<K,V>` and `CacheEvictedEventBusSpec` for cross-instance coherence and event buses.6364Use the repo's `createCacheEntry(): Pair<K, V>` contract:6566```kotlin67class MyDistributedCacheTest : DistributedCacheSpec<String>() {68 override fun createCache(): DistributedCache<String> {69 return MyDistributedCache()70 }7172 override fun createCacheEntry(): Pair<String, String> {73 return UUID.randomUUID().toString() to "test_value"74 }75}76```7778## Key Classes7980| Class | Module | Purpose |81|-------|--------|---------|82| `Cache<K,V>` | cocache-api | Base cache interface |83| `CoherentCache<K,V>` | cocache-core | Two-level cache engine |84| `DefaultCoherentCache` | cocache-core | Default implementation |85| `ClientSideCache<V>` | cocache-api | L2 local cache interface |86| `MapClientSideCache` | cocache-core | ConcurrentHashMap impl |87| `GuavaClientSideCache` | cocache-core | Guava Cache impl |88| `CaffeineClientSideCache` | cocache-core | Caffeine Cache impl |89| `DistributedCache<V>` | cocache-core | L1 distributed cache interface |90| `RedisDistributedCache` | cocache-spring-redis | Redis impl |91| `CacheSource<K,V>` | cocache-api | Data source loader |92| `CacheEvictedEventBus` | cocache-core | Event bus for coherence |93| `RedisCacheEvictedEventBus` | cocache-spring-redis | Redis Pub/Sub impl |94| `JoinCache<K1,V1,K2,V2>` | cocache-api | Composed cache interface |95| `SimpleJoinCache` | cocache-core | Default JoinCache impl |96| `KeyFilter` | cocache-core | Bloom filter for cache breakdown protection |97| `BloomKeyFilter` | cocache-core | Guava BloomFilter impl |9899## Build Commands100101```bash102./gradlew build -x test103./gradlew test104./gradlew :cocache-core:test105./gradlew :cocache-core:test --tests "me.ahoo.cache.proxy.ProxyCacheTest"106./gradlew :cocache-spring-redis:check107./gradlew :cocache-spring-boot-starter:check108./gradlew check109```