Cache
The module provides typed caches for storing computation results and reusable data,
so expensive operations do not have to run on every access. A cache can be used declaratively through method annotations
or imperatively through an injected interface, with local Caffeine and external Redis available as storage backends.
Local Caffeine is useful for fast in-process storage, while Redis is suitable for a shared cache used by several application instances.
The whole cache contract is synchronous: Cache<K, V> returns values directly, and cache aspects are applied to synchronous methods.
For a step-by-step walkthrough before the reference details, see Cache and Multi-Level Cache.
Caffeine¶
Implementation based on the Caffeine library for an in-memory application cache.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
Configuration¶
Example of a complete configuration for a cache at mycache.config; parameters are described in the CaffeineCacheConfig class (example values or default values are shown):
mycache {
config {
enabled = true //(1)!
expireAfterWrite = "10s" //(2)!
expireAfterAccess = "10s" //(3)!
initialSize = 10 //(4)!
maximumSize = 100000 //(5)!
telemetry {
logging {
enabled = false //(6)!
}
metrics {
enabled = false //(7)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(8)!
tags = { //(9)!
"key1" = "value1"
}
}
tracing {
enabled = true //(10)!
attributes = { //(11)!
"key1" = "value1"
}
}
}
}
}
- Enables the cache; when
falseevery cache operation becomes a no-op andcomputeIfAbsentalways calls the loader (default:true) - Time after which the value is removed from the cache; counted after the value is written (default not specified, optional)
- Time after which the value is removed from the cache; counted after the value is read (default not specified, optional)
- Initial cache size, helps avoid resizing when the number of values grows quickly (default not specified, optional)
- Maximum cache size; when the boundary is reached or slightly earlier, least relevant values are evicted (default:
100000) - Enables cache logging (default:
false) - Enables cache metrics; also controls whether the standard
MicrometerCaffeinemetrics are registered (default:false) - SLO configuration for metrics, values are durations and bare numbers mean milliseconds (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Tags configuration for metrics (default:
{}) - Enables cache tracing (default:
true) - Attributes configuration for tracing (default:
{})
mycache:
config:
enabled: true #(1)!
expireAfterWrite: "10s" #(2)!
expireAfterAccess: "10s" #(3)!
initialSize: 10 #(4)!
maximumSize: 100000 #(5)!
telemetry:
logging:
enabled: false #(6)!
metrics:
enabled: false #(7)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(8)!
tags: #(9)!
key1: value1
tracing:
enabled: true #(10)!
attributes: #(11)!
key1: value1
- Enables the cache; when
falseevery cache operation becomes a no-op andcomputeIfAbsentalways calls the loader (default:true) - Time after which the value is removed from the cache; counted after the value is written (default not specified, optional)
- Time after which the value is removed from the cache; counted after the value is read (default not specified, optional)
- Initial cache size, helps avoid resizing when the number of values grows quickly (default not specified, optional)
- Maximum cache size; when the boundary is reached or slightly earlier, least relevant values are evicted (default:
100000) - Enables cache logging (default:
false) - Enables cache metrics; also controls whether the standard
MicrometerCaffeinemetrics are registered (default:false) - SLO configuration for metrics, values are durations and bare numbers mean milliseconds (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Tags configuration for metrics (default:
{}) - Enables cache tracing (default:
true) - Attributes configuration for tracing (default:
{})
The underlying Caffeine cache is built by a CaffeineCacheFactory supplied as a @DefaultComponent.
If tuning beyond the configuration options above is required (for example custom eviction, weak keys, or a custom weigher),
register your own CaffeineCacheFactory component to override the default and customize the Caffeine builder directly.
@Component
public final class MyCaffeineCacheFactory implements CaffeineCacheFactory {
@Override
public <K, V> Cache<K, V> build(String name, CaffeineCacheConfig config) {
var builder = Caffeine.newBuilder().weakKeys();
if (config.expireAfterWrite() != null) {
builder.expireAfterWrite(config.expireAfterWrite());
}
builder.maximumSize(config.maximumSize());
return builder.build();
}
}
@Component
class MyCaffeineCacheFactory : CaffeineCacheFactory {
override fun <K, V> build(name: String, config: CaffeineCacheConfig): Cache<K, V> {
val builder = Caffeine.newBuilder().weakKeys()
config.expireAfterWrite()?.let { builder.expireAfterWrite(it) }
builder.maximumSize(config.maximumSize())
return builder.build()
}
}
Overriding the factory also replaces the default metric registration, so the standard Micrometer Caffeine metrics
have to be wired manually if they are still required.
Redis¶
Implementation based on in-memory database Redis and connection driver Lettuce.
Dependency¶
Dependency build.gradle:
Module:
Dependency build.gradle.kts:
Module:
LettuceRedisCacheModule is the entry point for the Redis cache: it extends RedisCacheModule and LettuceModule,
and provides the RedisCacheClient on top of the shared Lettuce connection.
The RedisCacheModule from the cache-redis-common artifact is transport-neutral: it contributes the cache telemetry factory,
key mappers, and value mappers, but does not provide a RedisCacheClient. It is useful only when a different Redis transport
is plugged in by supplying an own RedisCacheClient implementation.
Configuration¶
The Lettuce driver must be configured separately to connect to Redis.
A single connection is used for all Redis caches.
Basic Lettuce configuration parameters:
URIfor connecting toRedis(required, no default)- Command execution timeout (default:
30s)
Full Configuration
Example of a complete configuration for the Lettuce driver; parameters are described in the LettuceConfig class (example values or default values are shown):
lettuce {
uri = "redis://localhost:6379" //(1)!
user = "admin" //(2)!
password = "12345" //(3)!
database = 0 //(4)!
protocol = "RESP3" //(5)!
socketTimeout = "10s" //(6)!
commandTimeout = "30s" //(7)!
forceClusterClient = false //(8)!
ssl {
ciphers = [ "TLS_CHACHA20_POLY1305_SHA256" ] //(9)!
handshakeTimeout = "10s" //(10)!
}
telemetry {
logging {
enabled = false //(11)!
}
metrics {
enabled = false //(12)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(13)!
tags = { //(14)!
"key1" = "value1"
"key2" = "value2"
}
}
}
}
URIfor connecting toRedis(required, no default). Single-server connection:redis://localhost:6379. Multi-server connection:redis://localhost:6379,localhost:6380. Connection withSSL/TLS:rediss://localhost:6380.- Username for the connection (default not specified, optional)
- User password for the connection (default not specified, optional)
- Database number for the connection (default not specified, optional)
- Connection protocol, can be
RESP2orRESP3(default:RESP3) - Socket connection timeout (default:
10s) - Command execution timeout (default:
30s) - Create a cluster client even with a single connection
URI(default:false) - Cipher algorithms for a secure connection between client and server (default:
[]) - Timeout for establishing a secure connection with the server (default:
10s) - Enables driver logging (default:
false) - Enables driver metrics (default:
false) - SLO configuration for metrics (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Tags configuration for metrics (default:
{})
lettuce:
uri: "redis://localhost:6379" #(1)!
user: "admin" #(2)!
password: "12345" #(3)!
database: 0 #(4)!
protocol: "RESP3" #(5)!
socketTimeout: "10s" #(6)!
commandTimeout: "30s" #(7)!
forceClusterClient: false #(8)!
ssl:
ciphers:
- "TLS_CHACHA20_POLY1305_SHA256" #(9)!
handshakeTimeout: "10s" #(10)!
telemetry:
logging:
enabled: false #(11)!
metrics:
enabled: false #(12)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(13)!
tags: #(14)!
key1: value1
key2: value2
URIfor connecting toRedis(required, no default). Single-server connection:redis://localhost:6379. Multi-server connection:redis://localhost:6379,localhost:6380. Connection withSSL/TLS:rediss://localhost:6380.- Username for the connection (default not specified, optional)
- User password for the connection (default not specified, optional)
- Database number for the connection (default not specified, optional)
- Connection protocol, can be
RESP2orRESP3(default:RESP3) - Socket connection timeout (default:
10s) - Command execution timeout (default:
30s) - Create a cluster client even with a single connection
URI(default:false) - Cipher algorithms for a secure connection between client and server (default:
[]) - Timeout for establishing a secure connection with the server (default:
10s) - Enables driver logging (default:
false) - Enables driver metrics (default:
false) - SLO configuration for metrics (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Tags configuration for metrics (default:
{})
If a single URI is configured and forceClusterClient is false, a standalone RedisClient is created;
otherwise a RedisClusterClient is created for the list of URIs.
The Redis cache configuration defines behavior for a specific cache.
Example of a complete configuration for a cache at mycache.config; parameters are described in the RedisCacheConfig class (example values are shown):
mycache {
config {
enabled = true //(1)!
keyPrefix = "mykey" //(2)!
expireAfterWrite = "10s" //(3)!
expireAfterAccess = "10s" //(4)!
telemetry {
logging {
enabled = false //(5)!
}
metrics {
enabled = false //(6)!
slo = [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] //(7)!
tags = { //(8)!
"key1" = "value1"
}
}
tracing {
enabled = true //(9)!
attributes = { //(10)!
"key1" = "value1"
}
}
}
}
}
- Enables the cache; when
falseevery cache operation becomes a no-op andcomputeIfAbsentalways calls the loader (default:true) - Key prefix for the specific cache, used to avoid key collisions in one
Redisdatabase; can be an empty string, then keys will have no prefix (required, no default) - Sets the value expiration time on write (default not specified, optional)
- Sets the value expiration time on read (default not specified, optional)
- Enables cache logging (default:
false) - Enables cache metrics (default:
false) - SLO configuration for metrics (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Tags configuration for metrics (default:
{}) - Enables cache tracing (default:
true) - Attributes configuration for tracing (default:
{})
mycache:
config:
enabled: true #(1)!
keyPrefix: "mykey" #(2)!
expireAfterWrite: "10s" #(3)!
expireAfterAccess: "10s" #(4)!
telemetry:
logging:
enabled: false #(5)!
metrics:
enabled: false #(6)!
slo: [ 1, 10, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 30000, 60000, 90000 ] #(7)!
tags: #(8)!
key1: value1
tracing:
enabled: true #(9)!
attributes: #(10)!
key1: value1
- Enables the cache; when
falseevery cache operation becomes a no-op andcomputeIfAbsentalways calls the loader (default:true) - Key prefix for the specific cache, used to avoid key collisions in one
Redisdatabase; can be an empty string, then keys will have no prefix (required, no default) - Sets the value expiration time on write (default not specified, optional)
- Sets the value expiration time on read (default not specified, optional)
- Enables cache logging (default:
false) - Enables cache metrics (default:
false) - SLO configuration for metrics (default:
io.koraframework.telemetry.common.TelemetryConfig.MetricsConfig#DEFAULT_SLO) - Tags configuration for metrics (default:
{}) - Enables cache tracing (default:
true) - Attributes configuration for tracing (default:
{})
keyPrefix is required but may be an empty string. An empty prefix is reported with a warning at startup, because in that case
invalidateAll() cannot scan a prefix and falls back to FLUSHALL, which wipes the whole Redis database.
Module metrics are described in the Metrics Reference section, and the driver metrics
in the Redis / Lettuce section.
Custom cache telemetry is plugged in by registering your own RedisCacheTelemetryFactory or CaffeineCacheTelemetryFactory component,
which overrides the @DefaultComponent supplied by the module. To keep the default behavior and change only one part of it,
register a subclass of DefaultRedisCacheLoggerFactory / DefaultRedisCacheMetricsFactory
(or their Caffeine counterparts) — the default telemetry factory picks such components up as optional dependencies.
Key and Value Mappers¶
Redis stores keys and values as byte arrays, so RedisCache uses two kinds of mappers:
RedisCacheKeyMapper<K>turns a cache key intobyte[].RedisCacheValueMapper<V>writes a cache value tobyte[]and reads it back.
Regular keys are built through RedisCacheKeyMapper for the key type. Built-in mappers are available for String, byte[],
numbers, BigInteger, BigDecimal, UUID, Boolean, Character, Instant, LocalDateTime, LocalDate, ZonedDateTime,
Duration, Period, Enum, and Collection<T> when a mapper for T is also available.
For Enum, toString() is used, so it can be overridden when another key format is needed.
For values, built-in RedisCacheValueMapper implementations are available for the same simple types, date/time types, Enum, and byte[].
If another representation is needed, register your own RedisCacheValueMapper<V> or RedisCacheKeyMapper<K> component.
Both contracts are graph components, so a custom mapper must be annotated with @Component.
The common case is storing an object value as JSON. Annotate the value type with @Json so that Kora generates a JsonWriter
and JsonReader for it, and mark the value type argument of the cache contract with @Json as well: the built-in JSON value mapper
is registered under the @Json tag, so it is only injected where the value type argument carries that tag.
To use a different representation for such a type, drop the @Json tag from the type argument and register your own
RedisCacheValueMapper<V> component instead.
For a composite key based on a record or data class, Kora generates a separate RedisCacheKeyMapper for the whole key as a @DefaultComponent.
It receives a mapper for each field, converts every field to byte[], and joins the parts with RedisCacheKeyMapper.DELIMITER (:).
The part order matches the order of record components or data class properties.
For a key type that is not a record or data class, no mapper is generated and a RedisCacheKeyMapper for the key type has to be provided.
For a single key, built-in RedisCacheKeyMapper implementations can encode null as a special byte value.
In a composite key, each field mapping result must be non-null: if a custom RedisCacheKeyMapper for a field returns null,
key creation fails. For optional fields in a composite key, a custom mapper must explicitly encode null
as a stable byte value.
Configurer¶
The Lettuce client is assembled by LettuceFactory and can be customized before creation by registering Configurer components.
Configurer is io.koraframework.common.Configurer, a single-method contract T configure(T t).
Three builders can be customized: DefaultClientResources.Builder for shared client resources,
ClientOptions.Builder for a standalone client, and ClusterClientOptions.Builder for a cluster client.
@Component
public final class MyLettuceResourcesConfigurer implements Configurer<DefaultClientResources.Builder> {
@Override
public DefaultClientResources.Builder configure(DefaultClientResources.Builder builder) {
return builder.commandLatencyRecorder(CommandLatencyRecorder.disabled());
}
}
@Component
public final class MyLettuceOptionsConfigurer implements Configurer<ClientOptions.Builder> {
@Override
public ClientOptions.Builder configure(ClientOptions.Builder builder) {
return builder.autoReconnect(true);
}
}
@Component
class MyLettuceResourcesConfigurer : Configurer<DefaultClientResources.Builder> {
override fun configure(builder: DefaultClientResources.Builder): DefaultClientResources.Builder {
return builder.commandLatencyRecorder(CommandLatencyRecorder.disabled())
}
}
@Component
class MyLettuceOptionsConfigurer : Configurer<ClientOptions.Builder> {
override fun configure(builder: ClientOptions.Builder): ClientOptions.Builder {
return builder.autoReconnect(true)
}
}
For advanced scenarios beyond the typed cache, RedisCacheClient is available for injection as a low-level client that operates on raw byte[]
(scan/get/mget/getex/set/mset/psetex/del/flushAll) over the shared Lettuce connection; it is the client that RedisCache is built on top of.
Usage¶
Creating a cache will require registering a typed @Cache contract.
The contract interface must extend one of the Kora implementations: CaffeineCache or RedisCache.
For such an @Cache, an implementation is generated and added to the graph, so it can be injected as a dependency.
@Cache can only be applied to an interface, and that interface must extend exactly one of the two contracts —
extending both CaffeineCache and RedisCache is a compilation error.
The value argument in @Cache defines the full path to the configuration of the specific cache.
It points at the configuration object of that cache, so the config keys can live under a nested path such as mycache.config { ... },
or flat directly under the path such as my-cache { ... } as used in the example projects. Both forms are valid; pick one and keep the config keys under it.
The path must start with a letter, otherwise application generation fails.
Optional Values¶
If a Java method returns Optional<T>, the caching aspect can work with that signature directly.
The cache value type itself can be either T or Optional<T>:
CaffeineCache<String, String>and methodOptional<String> get(String key);CaffeineCache<String, Optional<String>>and methodString get(String key);CaffeineCache<String, Optional<String>>and methodOptional<String> get(String key).
For @Cacheable, this makes it possible to distinguish a missing cache entry from a method result that also means no data.
For @CachePut, the Optional<T> result is handled according to the cache value type: if the cache stores Optional<T>, the Optional itself is stored,
and if the cache stores T, only a present value is stored.
In Kotlin the same distinction is expressed with a nullable return type T?, and no Optional wrapper is used.
Imperative¶
Caches are available for injection as dependencies on the interface and can be used in conjunction with declarative operations.
Cache provides get(...), put(...), computeIfAbsent(...), invalidate(...), invalidateAll(),
as well as batch variants for a collection of keys or a map of values.
computeIfAbsent(...) methods first try to get a value from the cache; on a miss, they call the provided loader function and store the result.
CaffeineCache additionally provides getAll(), which returns every key and value currently held in memory.
RedisCache additionally provides the manual expiration methods described below.
RedisCache never propagates transport errors to the caller: a failed operation is recorded in telemetry and degrades
to a cache miss on read or to a silent no-op on write, so a Redis outage does not break the business method.
Composite Cache With Cache.Builder¶
If a composite cache is needed in imperative code, it can be built as a facade through Cache.Builder.
Layer order is defined by the add order: usually a fast local cache, such as Caffeine, is added first,
and a more shared cache, such as Redis, is added after it.
get(key)checks caches in order and returns the first found value.put(...),invalidate(...), andinvalidateAll()are executed in all caches.computeIfAbsent(...)checks caches in order; if a value is found in a lower layer, it is written into previous layers.- If the value is missing in every layer, the loader function is called and the result is written into all caches.
@Cache("mycache.caffeine.config")
public interface MyCaffeineCache extends CaffeineCache<String, String> { }
@Cache("mycache.redis.config")
public interface MyRedisCache extends RedisCache<String, String> { }
@KoraApp
public interface Application extends CaffeineCacheModule, LettuceRedisCacheModule {
default Cache<String, String> compositeCache(MyCaffeineCache caffeineCache, MyRedisCache redisCache) {
return Cache.builder(caffeineCache)
.addCache(redisCache)
.build();
}
}
@Cache("mycache.caffeine.config")
interface MyCaffeineCache : CaffeineCache<String, String>
@Cache("mycache.redis.config")
interface MyRedisCache : RedisCache<String, String>
@KoraApp
interface Application : CaffeineCacheModule, LettuceRedisCacheModule {
fun compositeCache(
caffeineCache: MyCaffeineCache,
redisCache: MyRedisCache,
): Cache<String, String> {
return Cache.builder(caffeineCache)
.addCache(redisCache)
.build()
}
}
The facade built through Cache.Builder does not support direct get(Collection<K>) and throws UnsupportedOperationException for it.
For batch loading, use computeIfAbsent(Collection<K>, Function<Set<K>, Map<K, V>>).
Manual Redis expiration¶
Beyond the shared Cache surface, RedisCache adds methods to override the configured expireAfterWrite for a single write.
putExpireAfterWrite(key, value, Duration) and its Map batch overload apply the provided Duration to that specific write
instead of the value from configuration. These methods are Redis-only.
@Cache("mycache.config")
public interface MyCache extends RedisCache<String, String> { }
@Component
public class SomeService {
private final MyCache cache;
public SomeService(MyCache cache) {
this.cache = cache;
}
public void cacheFor(String key, String value) {
cache.putExpireAfterWrite(key, value, Duration.ofMinutes(5));
}
}
Declarative¶
All aspect examples below assume the cache implementation above.
One method carries exactly one kind of cache operation. Mixing @Cacheable with @CachePut,
or @CacheInvalidate with @CacheInvalidateAll, on the same method is a compilation error.
Get¶
To cache and retrieve a value from the cache for the get() method, annotate it with @Cacheable.
If the value is found in the cache, the original method is not called; if there is no value, the method is executed and the result is stored in the cache.
The cache key is built from method arguments, and argument order matters. In this case it is built from arg1.
@Cacheable requires at least one method argument for the key; a method without arguments fails at compile time.
Put¶
To add values to the cache via the put() method, annotate it with @CachePut.
The method with @CachePut is always called, and its result is put into the cache defined in value.
The cache key is built from method arguments, and argument order matters. In this case it is built from arg1.
Invalidate¶
To remove a value from the cache by key via the evict() method, annotate it with @CacheInvalidate.
The method with @CacheInvalidate is called, and then the value is removed by key from the cache defined in value.
The cache key is built from method arguments, and argument order matters. In this case it is built from arg1.
Invalidate all¶
To remove all values from the cache via the evictAll() method, annotate it with @CacheInvalidateAll.
The method with @CacheInvalidateAll is called, and then all values are removed from the cache defined in value.
No cache key is built, so the method may take any arguments or none at all.
For a Redis cache, invalidateAll() scans the keys with the configured keyPrefix and deletes them.
If keyPrefix is an empty string, it falls back to FLUSHALL for the whole database.
Execution mode¶
Every cache annotation has a mode attribute of type CacheMode with two values:
CacheMode.SYNC(default) — the cache write happens on the calling thread before the method returns.CacheMode.ASYNC— the cache write is submitted to a dedicatedExecutorand the method returns without waiting for it.
ASYNC affects only the write side of the operation: put for @Cacheable and @CachePut,
invalidate for @CacheInvalidate, and invalidateAll for @CacheInvalidateAll.
The cache read performed by @Cacheable stays synchronous, because its result determines whether the original method is called.
The asynchronous operation runs on an Executor bound with @Tag(CacheMode.class).
CacheCommonModule provides it as a @DefaultComponent that starts a virtual thread per operation and logs a failed operation at WARN.
A custom @Tag(CacheMode.class) Executor component overrides that default.
ASYNC is ignored for CaffeineCache, since an in-memory write is not worth offloading; the processor reports it with a compilation warning.
Composite cache¶
If several caches need to be used, connect the required modules and specify several annotations on the method.
For example, this can combine a fast local layer on Caffeine and a shared layer on Redis.
And the annotated class itself:
The call order follows the order of annotations on the method from top to bottom.
For @Cacheable, this means the upper cache is checked first; on a miss, the next cache is checked,
and after the value is found in a lower layer it is written back into all previously checked layers.
If no layer holds the value, the original method is called and the result is written into every listed cache.
The same composition model works for repeatable @CachePut, @CacheInvalidate, and @CacheInvalidateAll: the method is called once,
and then the result is written to all listed caches or invalidation is executed in all listed caches.
The container annotations @Cacheables, @CachePuts, @CacheInvalidates, and @CacheInvalidateAlls can also be used when that form is more convenient.
All repeated annotations on one method must declare the same args list; different key argument lists on one method are a compilation error.
Key¶
If the cache key consists of one argument, register Cache with a signature that matches the key and value types.
Conversion¶
If an argument cannot be used directly as a cache key, the implementation requires a mapper
with the CacheKeyMapper interface. If there are two arguments for the key, CacheKeyMapper2 is required; if there are three, CacheKeyMapper3 is required, and so on up to CacheKeyMapper9.
More than nine key arguments are not supported.
Such a mapper can be provided manually with @Mapping. The mapper is injected into the generated aspect from the dependency graph,
so its class must be registered as a component with @Component — nested classes included.
Example of converting a complex object into a simple cache key:
@Component
public class SomeService {
public record UserContext(String userId, String traceId) { }
@Component
public static final class UserContextMapping implements CacheKeyMapper<String, UserContext> {
@Override
public String map(UserContext arg) {
return arg.userId();
}
}
@Mapping(UserContextMapping.class)
@Cacheable(MyCache.class)
public String get(UserContext context) {
// do something
}
}
@Component
open class SomeService {
data class UserContext(val userId: String, val traceId: String)
@Component
class UserContextMapping : CacheKeyMapper<String, UserContext> {
override fun map(arg: UserContext): String {
return arg.userId
}
}
@Mapping(UserContextMapping::class)
@Cacheable(MyCache::class)
open fun get(context: UserContext): String {
// do something
}
}
If several methods need different mappers with the same signature, the mapper components can be disambiguated
by adding @Tag next to @Mapping on the method and on the mapper component.
Composite key¶
If the cache key consists of several arguments, register Cache with a custom class
that describes that key.
Example for Cache where the composite key consists of two elements:
Create a custom record that describes the composite key.
The key is built by calling the public constructor of the key type whose parameter types match the method arguments in order.
If RedisCache is used, a RedisCacheKeyMapper is generated for the composite key.
It uses a mapper for each key field and expects the mapping result for every field to be non-null.
Built-in mappers can encode null with a special value, while custom mappers must do this explicitly.
Argument ordering¶
If the method accepts arguments that should be excluded from the composite key, or the argument order does not match
the order of the composite-key constructor arguments, use the args attribute and specify
which method arguments to use and in what order.
args defines the full set of method arguments used to build the key. Each name must match a method argument name,
and the order should match the key type: for a single argument, the Cache<K, V> key type; for a composite key,
the constructor argument order of the record or data class.
If a name does not match any method argument, application generation fails.
If the listed arguments do not fit any constructor of the key type — the order or the types differ —
the aspect falls back to a CacheKeyMapperN for that exact argument list and expects it in the dependency graph.
The build then fails at graph resolution unless such a mapper is registered, so either fix the argument order or supply a mapper with @Mapping.
Loadable Cache¶
The library provides the LoadableCache component, which combines get and put operations without using aspects.
It is useful when value loading must be controlled manually while keeping the standard logic: first check the cache,
and on a miss load the data and store it.
Cache.asLoadable(Function<Collection<K>, Map<K, V>>) builds a LoadableCache around a batch loader, and
Cache.asLoadableSimple(Function<K, V>) builds one around a single-key loader.
LoadableCache exposes get(K) and get(Collection<K>).
@Cache("mycache.config")
public interface MyCache extends CaffeineCache<String, String> { }
@KoraApp
public interface Application extends CaffeineCacheModule {
default LoadableCache<String, String> loadableCache(MyCache cache, SomeService someService) {
return cache.asLoadable(someService::loadEntities);
}
}
The same applies to RedisCache, since both cache contracts extend the common Cache interface.
Signatures¶
Available signatures for methods supported by annotations:
The class must not be final for aspects to work.
The T refers to the type of the return value.
T myMethod()Optional<T> myMethod()
@Cacheable and @CachePut require a return value and cannot be applied to void.
@CacheInvalidate and @CacheInvalidateAll can be applied to methods without a result.
Asynchronous and reactive return types are not supported by cache aspects:
CompletionStage<T>, Future<T>, Publisher<T>, Mono<T>, and Flux<T> are rejected at compile time.
The class must be open for aspects to work.
By T we mean the type of the return value, either T, T?, or Unit.
myMethod(): T
@Cacheable and @CachePut require a return value and cannot be applied to Unit.
@CacheInvalidate and @CacheInvalidateAll can be applied to methods without a result.
Asynchronous and reactive return types are not supported by cache aspects:
CompletionStage<T>, Future<T>, and Publisher<T> are rejected at compile time.