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:
- Construct bean name: Appends
.KeyConverterto the cache name from@CoCache(name = "..."). - Lookup bean: Calls
beanFactory.getBean(beanName)to retrieve the registered implementation. - 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 deterministictoStringKey()logic for your complex object. - Register as a Spring bean named exactly
<cacheName>.KeyConverterto enable discovery bySpringKeyConverterFactory. - 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →