How CoCache's Proxy-Based Caching Works Under the Hood
CoCache's proxy-based caching creates JDK dynamic proxies that wrap CoherentCache instances, using CoCacheInvocationHandler to intercept method calls and coordinate client-side, distributed, and source data layers without exposing complexity to callers.
CoCache (ahoo-wang/cocache) implements a sophisticated caching mechanism that leverages Java's dynamic proxy pattern to provide coherent caching capabilities. By generating runtime proxies through DefaultCacheProxyFactory, the framework transparently layers multiple cache tiers behind a simple interface, allowing developers to interact with what appears to be a standard cache while benefiting from automatic consistency management across distributed systems.
Core Architecture Components
The proxy-based system relies on several key components working in concert:
- CacheProxyFactory (
me.ahoo.cache.proxy.CacheProxyFactory): The factory interface responsible for creating proxy instances fromCoCacheMetadata. - DefaultCacheProxyFactory (
me.ahoo.cache.proxy.DefaultCacheProxyFactory): The concrete implementation that constructs theCoherentCacheand generates the JDK proxy. - CoCacheInvocationHandler (
me.ahoo.cache.proxy.CoCacheInvocationHandler): The invocation handler that routes method calls to the appropriate underlying implementation. - CoherentCache (
me.ahoo.cache.consistency.CoherentCache): The runtime engine that synchronizes client-side, distributed, and source caches.
The Proxy Creation Pipeline
CoCache's proxy instantiation follows a three-stage pipeline that separates cache construction from proxy generation.
Stage 1: Constructing the Coherent Cache
In DefaultCacheProxyFactory.create, the factory first assembles the multi-layered cache infrastructure:
val clientId = clientIdGenerator.generate()
val clientSideCaching: ClientSideCache<Any> = clientSideCacheFactory.create(cacheMetadata)
val distributedCaching: DistributedCache<Any> = distributedCacheFactory.create(cacheMetadata)
val cacheSource = cacheSourceFactory.create<Any, Any>(cacheMetadata)
val keyConverter = keyConverterFactory.create<Any>(cacheMetadata)
val delegate = coherentCacheFactory.create(
CoherentCacheConfiguration(
cacheName = cacheMetadata.cacheName,
clientId = clientId,
keyConverter = keyConverter,
clientSideCache = clientSideCaching,
distributedCache = distributedCaching,
cacheSource = cacheSource
)
)
This creates a CoherentCache delegate that manages three distinct tiers:
- Client-side cache: Local in-process storage for low-latency reads.
- Distributed cache: Redis or Hazelcast for cross-JVM consistency.
- Cache source: The authoritative data source (database or external service).
Stage 2: Generating the Dynamic Proxy
After constructing the delegate, DefaultCacheProxyFactory creates a JDK dynamic proxy that implements the user-defined interface plus internal mix-in interfaces:
val invocationHandler = CoCacheInvocationHandler(cacheMetadata, delegate)
return Proxy.newProxyInstance(
this.javaClass.classLoader,
arrayOf(
cacheMetadata.proxyInterface.java,
CoherentCache::class.java,
CacheDelegated::class.java,
CacheMetadataCapable::class.java
),
invocationHandler
) as CACHE
The proxy implements four interfaces:
- The user-defined cache interface (e.g.,
UserCache). CoherentCachefor accessing coherence operations.CacheDelegatedto expose the underlying delegate.CacheMetadataCapableto provide cache metadata access.
Stage 3: Method Interception and Routing
The CoCacheInvocationHandler intercepts all method invocations and applies specific routing logic:
- Cache operations (
get,put,invalidate): Forwarded to theCoherentCachedelegate. - Metadata queries (
cacheName(),keyConverter()): Served directly fromCoCacheMetadata. - Standard methods (
toString,hashCode,equals): Delegated to the underlying cache or default implementations.
This centralized handling ensures that all cache operations automatically benefit from coherent behavior—including client-side shortcuts, distributed synchronization, and source fallback—without requiring callers to manage these complexities.
Spring Framework Integration
CoCache provides first-class Spring support through dedicated configuration classes that automate proxy creation.
Auto-Configuration Components
The Spring integration layer includes:
- CacheProxyFactoryBean (
me.ahoo.cache.spring.proxy.CacheProxyFactoryBean): A Spring FactoryBean that creates cache proxies as managed beans. - EnableCoCacheRegistrar (
me.ahoo.cache.spring.EnableCoCacheRegistrar): Registers the proxy factory during context initialization. - CoCacheAutoConfiguration (
me.ahoo.cache.spring.boot.starter.CoCacheAutoConfiguration): Supplies theDefaultCacheProxyFactorybean with auto-configured dependencies.
Usage Example with Spring Boot
Define your cache interface:
interface UserCache {
fun getUser(id: String): User?
fun putUser(id: String, user: User)
}
Inject the proxy into your service:
@Service
class UserService(
@CacheProxyFactoryBean(UserCache::class) private val userCache: UserCache
) {
fun findUser(id: String): User? = userCache.getUser(id)
}
The @CacheProxyFactoryBean annotation triggers the creation of a CoherentCache-backed proxy that implements UserCache, automatically routing calls through the multi-layer caching stack.
Manual Proxy Creation
For non-Spring applications, instantiate the proxy factory directly:
val metadata = CoCacheMetadata(
cacheName = "userCache",
proxyInterface = UserCache::class,
// Additional configuration: TTL, key converter, etc.
)
val proxyFactory = DefaultCacheProxyFactory(
coherentCacheFactory,
clientIdGenerator,
clientSideCacheFactory,
distributedCacheFactory,
cacheSourceFactory,
keyConverterFactory
)
val userCache: UserCache = proxyFactory.create(metadata)
Once created, the proxy operates identically to the Spring-managed version, transparently coordinating cache layers:
val user = userCache.getUser("123") // Hits client-side → distributed → source
userCache.putUser("123", user) // Updates all layers coherently
Summary
- CoCache's proxy-based caching uses JDK dynamic proxies generated by
DefaultCacheProxyFactoryto wrapCoherentCacheinstances. - The CoCacheInvocationHandler routes method calls to appropriate handlers, separating cache operations from metadata queries.
- Each proxy implements four interfaces: the user API,
CoherentCache,CacheDelegated, andCacheMetadataCapable. - The underlying CoherentCache coordinates three tiers: client-side caching, distributed caching, and the authoritative cache source.
- Spring Boot integration automates proxy creation through
CacheProxyFactoryBeanandCoCacheAutoConfiguration.
Frequently Asked Questions
What is the role of CoCacheInvocationHandler?
CoCacheInvocationHandler serves as the dispatch center for all proxy method invocations. Located in me.ahoo.cache.proxy.CoCacheInvocationHandler, it intercepts calls to the proxy and determines whether to route them to the CoherentCache delegate (for cache operations), serve them from CoCacheMetadata (for configuration queries), or handle them as standard Object methods. This single handler eliminates the need for boilerplate delegation code while ensuring consistent coherent caching behavior across all operations.
How does CoCache handle cache misses across different layers?
When a get operation reaches the proxy, CoCacheInvocationHandler delegates to the CoherentCache instance, which implements a cascading read strategy. First, it checks the ClientSideCache for local data. If absent, it queries the DistributedCache (e.g., Redis). If still missing, it falls back to the CacheSource (database), then propagates the result back through the chain to populate the upper tiers. This automatic population ensures subsequent requests hit the faster layers.
Can CoCache be used without the Spring Framework?
Yes. While CoCache provides convenient Spring Boot auto-configuration through CoCacheAutoConfiguration and EnableCoCacheRegistrar, the core proxy mechanism in DefaultCacheProxyFactory has no Spring dependencies. You can manually construct the factory with your choice of CoherentCacheFactory, cache source implementations, and other components, then call create() with a CoCacheMetadata instance to obtain a functional cache proxy in plain Kotlin or Java applications.
What interfaces does the generated proxy implement?
Every proxy generated by DefaultCacheProxyFactory implements four interfaces: (1) the user-defined cache interface specified in CoCacheMetadata.proxyInterface, (2) CoherentCache to expose coherence operations, (3) CacheDelegated to allow access to the underlying delegate, and (4) CacheMetadataCapable to expose cache configuration like name and key converter. This multi-interface design allows both application code and framework infrastructure to interact with the proxy appropriately.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →