# How @JoinCacheable Enables Automatic Cache Joining in the Cocache Framework

> Discover how @JoinCacheable in Cocache automatically joins two independent caches into one cohesive unit. Learn the Spring-based metadata parsing and proxy generation pipeline behind this powerful feature.

- Repository: [Ahoo Wang/cocache](https://github.com/ahoo-wang/cocache)
- Tags: deep-dive
- Published: 2026-02-23

---

**The `@JoinCacheable` annotation triggers a Spring-based metadata parsing and proxy generation pipeline that automatically wires two independent caches into a single cohesive join cache without requiring manual implementation.**

The `cocache` library by Ahoo Wang simplifies distributed caching in JVM applications by providing declarative cache joining capabilities. By annotating a Kotlin interface with `@JoinCacheable`, developers can automatically combine data from two separate cache sources—referred to as the **first cache** and the **join cache**—into unified join values. This mechanism leverages Spring's bean registration lifecycle and dynamic proxy generation to eliminate boilerplate cache coordination code.

## Declaring Join Caches with @JoinCacheable

The foundation of automatic cache joining rests on a simple interface declaration paired with the `@JoinCacheable` annotation.

### The Annotation Contract

The [`JoinCacheable`](https://github.com/ahoo-wang/cocache/blob/main/cocache-api/src/main/kotlin/me/ahoo/cache/api/annotation/JoinCacheable.kt) annotation defines the relationship between two caches:

```kotlin
@JoinCacheable(
    firstCacheName = "UserExtendInfoCache",
    joinCacheName   = "UserCache",
    joinKeyExpression = "#{#root.userId}"
)
interface UserExtendInfoJoinCache :
        JoinCache<String, UserExtendInfo, String, User>

```

The annotation captures four critical pieces of metadata:
- **`firstCacheName`** – The primary cache holding the initial value
- **`joinCacheName`** – The secondary cache providing the joined data
- **`joinKeyExpression`** – A SpEL expression to extract the join key from the first value
- **`joinKeyExtractor`** – Optional bean name for a custom extractor (used when expression is blank)

### Interface Requirements

Any join cache interface must extend `JoinCache<K1, V1, K2, V2>` where:
- `K1` and `V1` represent the key and value types of the first cache
- `K2` and `V2` represent the key and value types of the join cache

## Parsing Metadata with JoinCacheMetadataParser

When the application context initializes, the framework converts annotation attributes into runtime metadata using [`JoinCacheMetadataParser`](https://github.com/ahoo-wang/cocache/blob/main/cocache-core/src/main/kotlin/me/ahoo/cache/annotation/JoinCacheMetadataParser.kt).

This parser performs three essential operations:
1. **Type Extraction** – Resolves the four generic type arguments from the interface declaration using `ResolvableType`
2. **Validation** – Ensures the target interface actually extends `JoinCache` and that cache names are non-blank
3. **Metadata Construction** – Builds a `JoinCacheMetadata` object containing the cache names, extractor configuration, and resolved type information

The resulting metadata object serves as the blueprint for proxy creation.

## Spring Bean Registration via EnableCoCacheRegistrar

The [`EnableCoCacheRegistrar`](https://github.com/ahoo-wang/cocache/blob/main/cocache-spring/src/main/kotlin/me/ahoo/cache/spring/EnableCoCacheRegistrar.kt) implements Spring's `ImportBeanDefinitionRegistrar` interface to hook into the annotation processing lifecycle. When the application detects `@EnableCoCache`, the registrar:

1. Scans for all interfaces annotated with `@JoinCacheable`
2. Invokes `JoinCacheMetadataParser.toJoinCacheMetadata()` to extract configuration
3. Registers a `JoinCacheProxyFactoryBean` definition for each discovered interface

This registration occurs during the `postProcessBeanFactory` phase, ensuring that join cache beans are available for dependency injection before the container finishes initialization.

## Dynamic Proxy Generation

The actual proxy instantiation is handled by [`JoinCacheProxyFactoryBean`](https://github.com/ahoo-wang/cocache/blob/main/cocache-spring/src/main/kotlin/me/ahoo/cache/spring/join/JoinCacheProxyFactoryBean.kt), which implements Spring's `FactoryBean` interface.

During the `getObject()` call:
1. The factory obtains a `JoinCacheProxyFactory` instance (the default implementation is `DefaultJoinCacheProxyFactory` provided by the core module)
2. Invokes `create(metadata)` to generate a dynamic proxy implementing the annotated interface
3. The proxy intercepts all method calls and delegates to the underlying cache coordination logic

The generated proxy automatically handles:
- Fetching the first value from the primary cache
- Resolving the join key using the configured extractor
- Fetching the join value from the secondary cache
- Assembling the results into a `JoinValue` object

## Resolving Join Keys with SpringJoinKeyExtractorFactory

Join key resolution represents the critical link between the two caches. The [`SpringJoinKeyExtractorFactory`](https://github.com/ahoo-wang/cocache/blob/main/cocache-spring/src/main/kotlin/me/ahoo/cache/spring/join/SpringJoinKeyExtractorFactory.kt) determines how to extract the key from the first cache's value:

**Strategy 1: SpEL Expression**
When `joinKeyExpression` is non-blank (e.g., `#{#root.userId}`), the factory creates an `ExpJoinKeyExtractor` that evaluates the expression against the first value at runtime.

**Strategy 2: Custom Bean**
When the expression is blank, the factory attempts to locate a Spring bean named `<cacheName>.JoinKeyExtractor`. For example, `UserExtendInfoJoinCache.JoinKeyExtractor` would resolve a custom extractor bean implementing `JoinKeyExtractor<V1, K2>`.

This dual strategy provides flexibility for simple property access via SpEL while supporting complex business logic through custom implementations.

## Practical Implementation Example

Consider a domain where `UserExtendInfo` joins with `User` via a user ID:

### Interface Definition

```kotlin
@JoinCacheable(
    firstCacheName = "UserExtendInfoCache",
    joinCacheName   = "UserCache",
    joinKeyExpression = "#{#root.userId}"
)
interface UserExtendInfoJoinCache :
        JoinCache<String, UserExtendInfo, String, User>

```

Source: [[`UserExtendInfoJoinCache.kt`](https://github.com/ahoo-wang/cocache/blob/main/UserExtendInfoJoinCache.kt)](https://github.com/ahoo-wang/cocache/blob/main/cocache-example/src/main/kotlin/me/ahoo/cache/example/cache/UserExtendInfoJoinCache.kt)

### Service Usage

```kotlin
@Service
class UserService(private val joinCache: UserExtendInfoJoinCache) {

    fun getUserWithInfo(extendInfoId: String): JoinValue<UserExtendInfo, String, User> {
        // The proxy automatically:
        // 1. Retrieves UserExtendInfo from UserExtendInfoCache
        // 2. Extracts userId using the SpEL expression
        // 3. Retrieves User from UserCache
        // 4. Returns a JoinValue combining both
        return joinCache[extendInfoId]
    }
}

```

### Custom Extractor Alternative

For scenarios requiring complex key resolution logic:

```kotlin
@Bean("UserExtendInfoJoinCache.JoinKeyExtractor")
fun customExtractor(): JoinKeyExtractor<UserExtendInfo, String> {
    return JoinKeyExtractor { extendInfo -> 
        extendInfo.userId.uppercase() // Complex transformation
    }
}

```

When this bean exists, `SpringJoinKeyExtractorFactory` uses it instead of the SpEL expression.

## Summary

- **`@JoinCacheable`** marks a Kotlin interface as a join cache, specifying the first cache, join cache, and key extraction strategy.
- **`JoinCacheMetadataParser`** transforms annotation attributes into runtime metadata objects containing type information and configuration.
- **`EnableCoCacheRegistrar`** automatically registers Spring bean definitions for each join cache interface during context initialization.
- **`JoinCacheProxyFactoryBean`** creates dynamic proxies that implement the join cache interface and coordinate dual cache access.
- **`SpringJoinKeyExtractorFactory`** resolves join keys either through SpEL evaluation or custom bean lookup, enabling flexible data relationships.

## Frequently Asked Questions

### What interface must a join cache implement?

A join cache interface must extend `JoinCache<K1, V1, K2, V2>` with four generic type parameters representing the key/value pairs of both the first and join caches. The interface requires no method implementations—only the `@JoinCacheable` annotation and type declarations.

### How does the framework resolve the join key at runtime?

The framework uses `SpringJoinKeyExtractorFactory` to create a `JoinKeyExtractor` instance. If the annotation specifies a `joinKeyExpression`, it builds an expression-based extractor using Spring's SpEL engine. Otherwise, it attempts to locate a custom extractor bean named `<cacheName>.JoinKeyExtractor` from the application context.

### Can I provide a custom join key extractor instead of using SpEL?

Yes. Define a bean implementing `JoinKeyExtractor<V1, K2>` and name it according to the pattern `<YourCacheInterfaceName>.JoinKeyExtractor`. For example, if your interface is `OrderCustomerJoinCache`, the bean name should be `OrderCustomerJoinCache.JoinKeyExtractor`. When the expression is blank, the factory automatically wires this custom extractor into the proxy.

### Where is the proxy implementation actually created?

The proxy implementation is created by `DefaultJoinCacheProxyFactory` (accessed via the `JoinCacheProxyFactory` interface) within the `JoinCacheProxyFactoryBean.getObject()` method. This factory generates a dynamic proxy that intercepts method calls, performs the dual cache lookups, and assembles the results, all while implementing the specific interface type declared by the developer.