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

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:

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:

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:

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:

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:

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) 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.

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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →