Skip to content

CoCache Introduction

CoCache is a Level 2 Distributed Coherence Cache Framework for Java/Kotlin that provides a two-level caching architecture with event-driven coherence across distributed instances. It is published under the group me.ahoo.cocache at version 4.0.2.

CoCache sits between your application and your data source, adding two cache layers -- a local in-memory L2 cache (Guava or Caffeine) and a shared distributed L1 cache (Redis) -- while keeping all instances coherent through an event bus.

Three-Tier Cache Concept

CoCache implements a three-tier data access model:

TierNameLocationPurposeSource
L2Client-side CacheIn-process (Guava / Caffeine)Fastest access, per-instancecocache-api/.../client/ClientSideCache.kt
L1Distributed CacheShared (Redis)Cross-instance consistencycocache-api/.../distributed/DistributedCache.kt
L0DataSourceOrigin (Database, API)Authoritative data sourcecocache-api/.../source/CacheSource.kt
mermaid
graph TB
    subgraph Application
        direction TB
        Caller["Caller<br>cache.get(key)"]
    end

    subgraph sg_20 ["L2 - Client-side Cache"]
        direction TB
        L2["Guava / Caffeine<br>In-process Memory"]
    end

    subgraph sg_21 ["L1 - Distributed Cache"]
        direction TB
        L1["Redis<br>Shared across instances"]
    end

    subgraph sg_22 ["L0 - DataSource"]
        direction TB
        L0["Database / API<br>Authoritative Source"]
    end

    Caller --> L2
    L2 -->|"cache miss"| L1
    L1 -->|"cache miss"| L0
    L0 -->|"return data"| L1
    L1 -->|"populate"| L2

    style Caller fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L0 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
mermaid
graph LR
    subgraph sg_23 ["Instance A"]
        L2A["L2 Cache<br>ClientSideCache"]
    end
    subgraph sg_24 ["Instance B"]
        L2B["L2 Cache<br>ClientSideCache"]
    end
    subgraph sg_25 ["Shared"]
        L1["L1 Cache<br>Redis"]
        EB["Event Bus<br>Redis Pub/Sub"]
    end

    L2A --> L1
    L2B --> L1
    L1 -->|"CacheEvictedEvent"| EB
    EB -->|"invalidate"| L2A
    EB -->|"invalidate"| L2B

    style L2A fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L2B fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style L1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style EB fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Key Features

FeatureDescriptionSource
Two-Level CachingL2 (local) + L1 (distributed) with fine-grained lockingDefaultCoherentCache.kt:89-135
Event-Driven CoherenceCacheEvictedEventBus for distributed cache invalidationCacheEvictedEventBus.kt
Annotation-Based Config@CoCache, @GuavaCache, @CaffeineCache, @JoinCacheablecocache-api/.../annotation/
JoinCacheCompose multiple cached values into a single resultJoinCache.kt
Cache Stampede PreventionPer-key synchronized locking prevents thundering herdDefaultCoherentCache.kt:78-86
Cache Penetration GuardMissingGuard caches null values to prevent repeated DB hitsMissingGuard.kt
Bloom Key FilterOptional Bloom filter to block non-existent key queriesBloomKeyFilter.kt
TTL JitterRandom TTL amplitude prevents cache avalancheComputedTtlAt.kt:49-56
Proxy-Based CachingDynamic proxies implement cache interfaces at runtimeCoCacheProxy.kt
Spring Boot StarterAuto-configuration with conditional bean registrationCoCacheAutoConfiguration.kt:61-186

Architecture Overview

mermaid
graph TB
    subgraph sg_26 ["Spring Boot Application"]
        direction TB
        EnableCoCache["@EnableCoCache<br>caches = [UserCache::class]"]
        UserCache["UserCache Interface<br>@CoCache + @GuavaCache"]
        Proxy["CoCacheProxy<br>InvocationHandler"]
    end

    subgraph sg_27 ["CoCache Core"]
        direction TB
        CoherentCache["DefaultCoherentCache<br>L2 + L1 + Event"]
        KeyConverter["KeyConverter<br>key -> cacheKey"]
        MissingGuard["MissingGuard<br>null value protection"]
    end

    subgraph sg_28 ["L2 - Client Side"]
        direction TB
        Guava["GuavaClientSideCache"]
        Caffeine["CaffeineClientSideCache"]
        Map["MapClientSideCache"]
    end

    subgraph sg_29 ["L1 - Distributed"]
        direction TB
        Redis["RedisDistributedCache"]
    end

    subgraph sg_30 ["Coherence"]
        direction TB
        EventBus["CacheEvictedEventBus<br>Redis Pub/Sub"]
    end

    EnableCoCache --> UserCache
    UserCache --> Proxy
    Proxy --> CoherentCache
    CoherentCache --> KeyConverter
    CoherentCache --> MissingGuard
    CoherentCache --> Guava
    CoherentCache --> Caffeine
    CoherentCache --> Map
    CoherentCache --> Redis
    CoherentCache --> EventBus

    style EnableCoCache fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style UserCache fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Proxy fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style CoherentCache fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style KeyConverter fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style MissingGuard fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Guava fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Caffeine fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Map fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Redis fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style EventBus fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Caching Flow

mermaid
sequenceDiagram
autonumber
    participant Caller as Caller
    participant L2 as L2 Cache
    participant KeyFilter as KeyFilter
    participant L1 as L1 Redis
    participant Lock as KeyLock
    participant L0 as DataSource
    participant EventBus as EventBus

    Caller->>L2: get(key)
    alt L2 hit
        L2-->>Caller: CacheValue
    else L2 miss
        L2-->>Caller: null
        Caller->>KeyFilter: notExist(key)?
        alt key definitely not exist
            KeyFilter-->>Caller: MissingGuard (null)
        else key may exist
            Caller->>L1: getCache(key)
            alt L1 hit
                L1-->>Caller: CacheValue
                Caller->>L2: setCache(key, value)
            else L1 miss
                Caller->>Lock: synchronized(lock)
                Lock->>L2: getCache(key) [double-check]
                Lock->>L1: getCache(key) [double-check]
                Lock->>L0: loadCacheValue(key)
                L0-->>Lock: CacheValue or null
                alt value found
                    Lock->>L2: setCache(key, value)
                    Lock->>L1: setCache(key, value)
                    Lock->>EventBus: publish(CacheEvictedEvent)
                else value not found
                    Lock->>L2: setCache(key, MissingGuard)
                    Lock->>L1: setCache(key, MissingGuard)
                end
                Lock-->>Caller: CacheValue or null
            end
        end
    end

Module Architecture

ModuleDescriptionSource
cocache-apiCore interfaces (Cache, CacheValue, ClientSideCache, CacheSource)cocache-api/
cocache-coreDefault implementations (DefaultCoherentCache, proxy-based caching)cocache-core/
cocache-springSpring integration (@EnableCoCache, factory beans)cocache-spring/
cocache-spring-redisRedis distributed cache implementationcocache-spring-redis/
cocache-spring-cacheSpring Cache abstraction bridgecocache-spring-cache/
cocache-spring-boot-starterAuto-configuration for Spring Bootcocache-spring-boot-starter/
cocache-testShared test specs (TCK)cocache-test/
cocache-exampleExample applicationcocache-example/
cocache-bomBill of Materialscocache-bom/
cocache-dependenciesCentralized version catalogcocache-dependencies/

Project Information

PropertyValueSource
Groupme.ahoo.cocachegradle.properties:14
Version4.0.2gradle.properties:15
LicenseApache License 2.0gradle.properties:23
JDK17+ (via jvmToolchain)build.gradle.kts
Gradle9.4.1 (wrapper)gradle/wrapper/gradle-wrapper.properties

Quick Example

kotlin
// 1. Define a cache interface
@CoCache(keyPrefix = "user:", ttl = 120)
@GuavaCache(
    maximumSize = 1_000_000,
    expireUnit = TimeUnit.SECONDS,
    expireAfterAccess = 120
)
interface UserCache : Cache<String, User>

// 2. Enable caching
@EnableCoCache(caches = [UserCache::class])
@SpringBootApplication
class AppServer

// 3. Use the cache
@RestController
class UserController(private val userCache: UserCache) {
    @GetMapping("{id}")
    fun get(@PathVariable id: String): User? = userCache[id]
}

Released under the Apache License 2.0.