# How to Implement a Custom KeyConverter for Complex Object Keys in CoCache

> Learn to implement a custom KeyConverter for complex object keys in CoCache. Register your converter as a Spring bean to ensure stable cache identifiers and boost performance.

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

---

**Implement the `KeyConverter<K>` interface and register it as a Spring bean named `<cacheName>.KeyConverter` to enable deterministic conversion of complex object keys into stable string cache identifiers.**

CoCache stores cache entries by converting logical key objects into `String` identifiers. By default, the library uses `ToStringKeyConverter` (plain `toString()`) or `ExpKeyConverter` (SpEL expression), but these defaults break down when your key is a composite object containing multiple fields. Implementing a custom KeyConverter for complex object keys ensures collision-free serialization tailored to your domain model.

## Implementing the KeyConverter Interface

CoCache defines the conversion contract in `me.ahoo.cache.converter.KeyConverter`. This single-method functional interface requires you to implement `toStringKey(sourceKey: K): String`, which must return a deterministic, unique string representation for every distinct key instance.

### Define Your Complex Key Class

First, model your composite identifier as a data class or POJO. This example uses a composite order key containing a tenant identifier and an order number:

```kotlin
package com.example.cache

/** Complex key containing multiple fields that uniquely identify an order. */
data class OrderKey(val tenantId: String, val orderId: Long)

```

### Create the Converter Implementation

Next, implement `KeyConverter<OrderKey>` to define how the complex object maps to a cache string. Choose a delimiter that cannot appear in your field values, or use a hash-based encoding if necessary:

```kotlin
package com.example.cache

import me.ahoo.cache.converter.KeyConverter

/**
 * Custom converter joining tenant and order ID with a colon delimiter.
 * The implementation must be deterministic and collision-free.
 */
class OrderKeyConverter : KeyConverter<OrderKey> {
    
    override fun toStringKey(sourceKey: OrderKey): String =
        "${sourceKey.tenantId}:${sourceKey.orderId}"
}

```

**Key implementation requirements:**
- The conversion logic must be **deterministic**—the same key object must always produce the same string.
- The output must be **collision-free** within your key domain to prevent cache overwrites.
- The converter should remain stateless; it is a pure function of the input key.

## Registering the Converter as a Spring Bean

CoCache discovers custom converters through `SpringKeyConverterFactory`, which builds bean names by appending `KEY_CONVERTER_SUFFIX` (defined as `.KeyConverter`) to the cache name. For a cache named `orderCache`, the factory searches for a bean named exactly `orderCache.KeyConverter`.

Register your implementation using Spring’s `@Bean` annotation with the precise name:

```kotlin
package com.example.config

import com.example.cache.OrderKeyConverter
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class CacheKeyConverterConfig {

    /** 
     * Bean name must match "<cacheName>.KeyConverter" 
     * as resolved by SpringKeyConverterFactory.
     */
    @Bean("orderCache.KeyConverter")
    fun orderKeyConverter(): OrderKeyConverter = OrderKeyConverter()
}

```

If you manage multiple caches requiring custom key conversion, declare a separate bean for each cache name following the same naming convention.

## Configuring the Cache Interface

Apply the `@CoCache` annotation to your cache interface, specifying the cache name that matches your bean registration. CoCache automatically wires the custom converter during bean instantiation:

```kotlin
package com.example.cache

import me.ahoo.cache.annotation.CoCache
import me.ahoo.cache.api.Cache

/** 
 * Cache storing Order objects keyed by the composite OrderKey.
 * The custom converter is automatically applied to all get/put/evict operations.
 */
@CoCache(name = "orderCache")
interface OrderCache : Cache<OrderKey, Order>

```

When the `OrderCache` bean is created, `SpringKeyConverterFactory.create(cacheMetadata)` retrieves the `orderCache.KeyConverter` bean from the `ApplicationContext` and injects it into the generated `CoherentCache` instance. All subsequent cache operations transparently convert `OrderKey` instances to strings like `"tenantA:12345"`.

## Testing the Key Conversion

Verify your implementation with unit tests to ensure deterministic behavior:

```kotlin
package com.example.cache

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class OrderKeyConverterTest {

    private val converter = OrderKeyConverter()

    @Test
    fun `convert complex key to string`() {
        val key = OrderKey("tenantA", 12345L)
        assertEquals("tenantA:12345", converter.toStringKey(key))
    }

    @Test
    fun `different keys produce different strings`() {
        val key1 = OrderKey("tenantA", 12345L)
        val key2 = OrderKey("tenantB", 12345L)
        assertEquals(false, converter.toStringKey(key1) == converter.toStringKey(key2))
    }
}

```

## How CoCache Discovers Custom Converters

The internal resolution flow in `SpringKeyConverterFactory` (located in [`cocache-spring/src/main/kotlin/me/ahoo/cache/spring/converter/SpringKeyConverterFactory.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-spring/src/main/kotlin/me/ahoo/cache/spring/converter/SpringKeyConverterFactory.kt)) follows this sequence:

1. **Construct bean name**: Appends `.KeyConverter` to the cache name from `@CoCache(name = "...")`.
2. **Lookup bean**: Calls `beanFactory.getBean(beanName)` to retrieve the registered implementation.
3. **Cast and store**: Casts the bean to `KeyConverter<K>` and stores it in the cache proxy.

This mechanism is wired automatically by `CoCacheAutoConfiguration` in the Spring Boot starter, requiring no manual factory configuration.

## Summary

- **Implement `KeyConverter<K>`** to define deterministic `toStringKey()` logic for your complex object.
- **Register as a Spring bean** named exactly `<cacheName>.KeyConverter` to enable discovery by `SpringKeyConverterFactory`.
- **Annotate your cache interface** with `@CoCache(name = "<cacheName>")` to trigger automatic converter injection.
- **Test thoroughly** to guarantee collision-free serialization across your key domain.

## Frequently Asked Questions

### What interface must I implement to create a custom key converter?

You must implement `me.ahoo.cache.converter.KeyConverter<K>`, a functional interface declaring `fun toStringKey(sourceKey: K): String`. This interface is located in [`cocache-core/src/main/kotlin/me/ahoo/cache/converter/KeyConverter.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-core/src/main/kotlin/me/ahoo/cache/converter/KeyConverter.kt).

### How does CoCache discover my custom KeyConverter implementation?

CoCache uses `SpringKeyConverterFactory` to construct a bean name by appending `.KeyConverter` to your cache name (e.g., `orderCache.KeyConverter`). It then queries the Spring `ApplicationContext` for that specific bean name and casts it to `KeyConverter<K>`.

### Can I use the same KeyConverter for multiple caches?

Yes, but you must register separate bean instances (or aliases) for each cache name. Each cache looks for its own specific bean name following the `<cacheName>.KeyConverter` convention. You can delegate to a shared implementation class to avoid code duplication.

### What happens if no custom KeyConverter bean is defined?

If `SpringKeyConverterFactory` cannot find a bean matching `<cacheName>.KeyConverter`, CoCache falls back to default converters. It will use `ToStringKeyConverter` (calling `key.toString()`) or `ExpKeyConverter` if a SpEL expression is configured, which may produce incorrect keys for complex objects lacking proper `toString()` implementations.