# How to Implement a Custom CacheSource for Database-Backed Cache Loading in CoCache

> Learn to implement a custom CacheSource for database-backed cache loading in CoCache. CoCache automatically loads data from your database on cache misses.

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

---

**Implement the `CacheSource<K, V>` interface (and optionally `TtlConfigurationAware`), expose the implementation as a Spring bean whose name ends with `.CacheSource`, and CoCache will automatically invoke `loadCacheValue(key)` on cache misses to fetch data from your database.**

CoCache is a high-performance, multi-level caching framework for JVM applications that combines client-side and distributed stores. When a cache miss occurs, the framework delegates to a **CacheSource** to load the missing value from an external system. By default, CoCache uses a no-op implementation, but you can create a **custom CacheSource for database-backed cache loading in CoCache** to seamlessly integrate with JPA, MyBatis, or JDBC repositories.

## Understanding the CacheSource Interface Contract

The core abstraction for loading data is the `CacheSource<K, V>` interface defined in [`cocache-api/src/main/kotlin/me/ahoo/cache/api/source/CacheSource.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-api/src/main/kotlin/me/ahoo/cache/api/source/CacheSource.kt). This contract requires only a single method:

```kotlin
@Throws(TimeoutException::class)
fun loadCacheValue(key: K): CacheValue<V>?

```

The method receives the cache key and returns a `CacheValue<V>` wrapper containing the loaded entity, or `null` if the key does not exist in the underlying store. If the operation times out, it should throw `TimeoutException` to signal the failure.

When no custom implementation is provided, CoCache falls back to `NoOpCacheSource`, located at [`cocache-api/src/main/kotlin/me/ahoo/cache/api/source/NoOpCacheSource.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-api/src/main/kotlin/me/ahoo/cache/api/source/NoOpCacheSource.kt). This default implementation always returns `null`, effectively disabling automatic cache loading.

## Implementing a Database-Backed CacheSource

To fetch data from a relational database, create a class that implements `CacheSource` and optionally `TtlConfigurationAware` to respect the TTL configuration defined in your `@CoCache` annotation.

### Creating the Implementation Class

The following example demonstrates a complete implementation using Spring Data JPA. This pattern mirrors the `CustomizeUserCacheSource` found in the CoCache test suite at [`cocache-spring-boot-starter/src/test/kotlin/me/ahoo/cache/spring/boot/starter/customize/EnableCoCacheConfigurationWithCustomize.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-spring-boot-starter/src/test/kotlin/me/ahoo/cache/spring/boot/starter/customize/EnableCoCacheConfigurationWithCustomize.kt).

```kotlin
package com.example.cache

import me.ahoo.cache.api.CacheValue
import me.ahoo.cache.api.source.CacheSource
import me.ahoo.cache.TtlConfigurationAware
import me.ahoo.cache.TtlConfiguration
import org.springframework.stereotype.Component
import java.util.concurrent.TimeoutException

@Component
class UserCacheSource(
    private val userRepository: UserRepository
) : CacheSource<String, User>, TtlConfigurationAware {

    private var ttlConfig: TtlConfiguration? = null

    @Throws(TimeoutException::class)
    override fun loadCacheValue(key: String): CacheValue<User>? {
        val user = userRepository.findById(key).orElse(null) ?: return null
        return CacheValue.of(user, ttlConfig?.ttl?.toMillis())
    }

    override fun setTtlConfiguration(ttlConfiguration: TtlConfiguration) {
        this.ttlConfig = ttlConfiguration
    }
}

```

### Handling TTL Configuration

By implementing `TtlConfigurationAware`, your source receives the TTL duration configured via the `@CoCache` annotation (e.g., `ttl = "30s"`). The `setTtlConfiguration` callback occurs during cache initialization, allowing you to store the TTL and apply it to the `CacheValue` returned from `loadCacheValue`. This ensures consistency between the database-loaded value and the cache expiration policy.

### Querying the Database

Inside `loadCacheValue`, integrate with your persistence layer using JPA, MyBatis, or raw JDBC. Wrap the resulting entity in `CacheValue.of(value, ttlMillis)` to preserve both the payload and its time-to-live. Return `null` for missing keys to indicate a cache miss, allowing CoCache to handle the negative caching strategy according to your configuration.

## Registering the Source as a Spring Bean

When using the Spring Boot starter, CoCache automatically discovers your implementation via `SpringCacheSourceFactory`, located at [`cocache-spring/src/main/kotlin/me/ahoo/cache/spring/source/SpringCacheSourceFactory.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-spring/src/main/kotlin/me/ahoo/cache/spring/source/SpringCacheSourceFactory.kt).

The factory resolves beans by type using the generic parameters `K` and `V` from your `CacheSource` implementation. Crucially, the bean name must end with the suffix `.CacheSource` (defined as `CACHE_SOURCE_SUFFIX` in the factory). In the example above, the class name `UserCacheSource` satisfies this convention. Alternatively, you can explicitly name the bean:

```kotlin
@Bean("userCacheSource.CacheSource")
fun userCacheSource(repo: UserRepository) = UserCacheSource(repo)

```

If no matching bean is found, the factory's `fallback()` method returns `CacheSource.noOp()`, ensuring the application starts without errors but without automatic loading capabilities.

## How CoCache Integrates Your Custom Source

The core cache implementation, `CoherentCache` in [`cocache-core/src/main/kotlin/me/ahoo/cache/consistency/CoherentCache.kt`](https://github.com/ahoo-wang/cocache/blob/main/cocache-core/src/main/kotlin/me/ahoo/cache/consistency/CoherentCache.kt), holds a reference to your `CacheSource`. When a `get(key)` operation results in a miss across all cache levels, CoCache invokes `loadCacheValue(key)`.

If your implementation returns a non-null `CacheValue`, CoCache performs the following actions:

1. Stores the value in the client-side cache (e.g., Caffeine or Guava)
2. Propagates the value to the distributed cache (e.g., Redis) if configured
3. Returns the value to the caller

This mechanism ensures that subsequent requests for the same key are served from the fast in-memory or distributed tiers, while your database remains the authoritative source of truth.

## Summary

- **Implement `CacheSource<K, V>`** to define how missing keys are loaded from your database or external store.
- **Optionally implement `TtlConfigurationAware`** to respect the TTL settings defined in `@CoCache` annotations.
- **Register as a Spring bean** with the `.CacheSource` suffix so `SpringCacheSourceFactory` can discover and inject your implementation.
- **Return `CacheValue`** from `loadCacheValue` to populate both client-side and distributed caches, or return `null` for missing keys.
- **Reference implementation:** Study `CustomizeUserCacheSource` in the CoCache test suite for a production-ready pattern.

## Frequently Asked Questions

### What happens if my CacheSource throws a TimeoutException?

If `loadCacheValue` throws `TimeoutException`, CoCache propagates the error to the caller rather than caching a null value. This prevents stale data from being stored when the database is temporarily unavailable. You should implement retry logic or circuit breakers within your source implementation if transient failures are expected.

### Can I use multiple CacheSource implementations for different entity types?

Yes. CoCache resolves the correct `CacheSource` based on the generic type parameters `K` (key type) and `V` (value type). Each cache defined with `@EnableCoCache` can have its own dedicated source bean, provided the generic types match the cache declaration. For example, a `UserCache` using `Cache<String, User>` will receive the `CacheSource<String, User>` bean.

### Do I need to handle caching the loaded value myself?

No. Once your `loadCacheValue` method returns a `CacheValue`, the `CoherentCache` implementation automatically handles writing to the client-side cache and the distributed cache (if configured). You only need to focus on fetching the data from your database; CoCache manages the storage and expiration logic.

### What is the performance impact of implementing a custom CacheSource?

The performance depends entirely on your database query efficiency. CoCache invokes `loadCacheValue` synchronously on the calling thread during a cache miss, so slow queries will block the request. Consider adding timeouts, asynchronous preloading, or query optimization to minimize latency. The framework itself adds minimal overhead beyond the method invocation.