How to Handle Cache Serialization with Different Codecs in CoCache

CoCache abstracts cache value serialization through the CodecExecutor<V> interface, allowing you to configure built-in strategies like JSON or Redis Hash encoding, or implement custom binary formats by extending AbstractCodecExecutor.

CoCache (ahoo-wang/cocache) provides a flexible, pluggable serialization layer that decouples your domain objects from Redis storage formats. Whether you need human-readable JSON for debugging or compact binary serialization for high-performance scenarios, understanding how to handle cache serialization with different codecs in CoCache enables you to optimize both storage efficiency and read/write performance.

The CodecExecutor Contract

At the core of CoCache’s serialization strategy lies the CodecExecutor<V> interface defined in cocache-spring-redis/src/main/kotlin/me/ahoo/cache/spring/redis/codec/CodecExecutor.kt. This contract standardizes how cache values are transformed before persistence and reconstructed upon retrieval.

The interface specifies two essential operations:

interface CodecExecutor<V> {
    /** Decode the cached entry back to a CacheValue */
    fun executeAndDecode(key: String, ttlAt: Long): CacheValue<V>

    /** Encode the CacheValue before persisting */
    fun executeAndEncode(key: String, cacheValue: CacheValue<V>)
}

The executeAndEncode method receives the cache key and the CacheValue<V> wrapper containing your domain object and TTL metadata, allowing you to serialize the value into the format required by your storage backend. Conversely, executeAndDecode handles deserialization using the key and expiration timestamp.

Built-in Serialization Strategies

CoCache ships with five pre-built executors in the cocache-spring-redis module, each optimized for specific data types and access patterns:

  • StringToStringCodecExecutor (StringToStringCodecExecutor.kt): Passes string values through without transformation. Use this when caching raw text content like Markdown or pre-serialized JSON strings.

  • ObjectToJsonCodecExecutor (ObjectToJsonCodecExecutor.kt): Converts POJOs to JSON using Jackson. This is the default choice for most domain objects, providing human-readable storage and easy debugging.

  • ObjectToHashCodecExecutor (ObjectToHashCodecExecutor.kt): Maps object properties to Redis hash fields. This enables partial updates and field-level TTL management rather than rewriting entire objects.

  • MapToHashCodecExecutor: Persists Map<K,V> structures directly as Redis hashes, ideal for dictionary-like cache entries.

  • SetToSetCodecExecutor: Stores Set<E> collections using native Redis set commands, preserving set semantics and supporting set-based operations like intersections.

All concrete implementations extend AbstractCodecExecutor (AbstractCodecExecutor.kt), which provides common Redis helper methods and template operations to simplify custom implementations.

Configuring Codecs in RedisDistributedCache

When constructing a RedisDistributedCache (or using RedisDistributedCacheFactory), you inject the desired executor via the constructor. This design allows different cache instances within the same application to use different serialization strategies.

To configure JSON serialization for a User object cache:

@Bean
fun userCache(redisTemplate: RedisTemplate<String, String>): DistributedCache<User> {
    val codecExecutor = ObjectToJsonCodecExecutor<User>()
    return RedisDistributedCache(redisTemplate, codecExecutor)
}

To switch to hash-based storage for the same object type:

val codecExecutor = ObjectToHashCodecExecutor<User>()
return RedisDistributedCache(redisTemplate, codecExecutor)

The RedisDistributedCache class delegates all serialization concerns to the supplied executor, keeping the cache implementation agnostic of the actual storage format.

Implementing Custom Codecs

For proprietary binary formats or third-party serializers like Kryo or Protobuf, extend AbstractCodecExecutor and implement the two abstract methods. Here is a complete Kryo implementation:

class KryoCodecExecutor<V : Any>(private val kryo: Kryo) : AbstractCodecExecutor<V>() {
    override fun executeAndEncode(key: String, cacheValue: CacheValue<V>) {
        val bytes = ByteArrayOutputStream().use { bos ->
            kryo.writeObject(bos, cacheValue.value)
            bos.toByteArray()
        }
        redisTemplate.opsForValue().set(
            key, 
            bytes, 
            cacheValue.ttlAt, 
            TimeUnit.SECONDS
        )
    }

    override fun executeAndDecode(key: String, ttlAt: Long): CacheValue<V> {
        val bytes = redisTemplate.opsForValue().get(key) 
            ?: return CacheValue.empty()
        val value = ByteArrayInputStream(bytes).use { bis -> 
            kryo.readObject(bis, V::class.java) 
        }
        return CacheValue(value, ttlAt)
    }
}

Register the custom executor as you would a built-in one:

@Bean
fun userCacheKryo(redisTemplate: RedisTemplate<String, ByteArray>): DistributedCache<User> {
    val kryo = Kryo().apply { register(User::class.java) }
    val codecExecutor = KryoCodecExecutor<User>(kryo)
    return RedisDistributedCache(redisTemplate, codecExecutor)
}

Summary

  • CodecExecutor defines the contract for encoding and decoding cache values in CodecExecutor.kt.
  • Choose ObjectToJsonCodecExecutor for standard POJO serialization, ObjectToHashCodecExecutor for partial updates, or StringToStringCodecExecutor for raw text.
  • Supply your selected executor to RedisDistributedCache or RedisDistributedCacheFactory to wire the serialization strategy.
  • Extend AbstractCodecExecutor to implement custom binary codecs using libraries like Kryo or Protobuf.

Frequently Asked Questions

What is the default codec for object serialization in CoCache?

The ObjectToJsonCodecExecutor serves as the default strategy for most domain objects, utilizing Jackson to map POJOs to JSON strings. This provides a balance of human readability and broad compatibility across different services and languages.

Can different cache instances use different codecs in the same application?

Yes. Because the codec is injected into RedisDistributedCache via its constructor, you can declare multiple cache beans—each configured with a different CodecExecutor implementation. One cache might use JSON for debugging while another uses Kryo for performance-critical data.

Which codec supports partial updates of cached objects?

The ObjectToHashCodecExecutor stores each object property as a separate Redis hash field, enabling you to update individual fields without rewriting the entire cache entry. This also supports field-level TTL configurations that are not possible with string-based serialization.

How do I handle schema evolution when using custom binary codecs?

When implementing custom executors by extending AbstractCodecExecutor, you control the serialization format entirely. For schema evolution with binary formats like Kryo or Protobuf, register versioned serializers within your codec’s initialization logic, or implement a migration strategy in the executeAndDecode method to handle legacy formats gracefully.

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 →