skip to content

What is the CacheResolver SPI, and how does CachingConfigurer let you customize resolution globally?

level: seniorimportance: should knowfreq 35%

answer

  1. CacheResolver.resolveCaches(context) -> Collection<Cache>
  2. context: target, method, args, operation
  3. SimpleCacheResolver = default over one CacheManager
  4. CachingConfigurer.cacheResolver()/cacheManager()/errorHandler()
  5. CachingConfigurerSupport deprecated in Spring 6

basics

~20 s

CacheResolver decides at runtime which Cache instances an operation uses, given the invocation context (target, method, args). The default SimpleCacheResolver just delegates to a CacheManager. Implement CachingConfigurer to supply a custom cacheResolver (or cacheManager, keyGenerator, errorHandler) globally.

solid answer

~40 s

By default Spring resolves caches statically from cache names against a single CacheManager via SimpleCacheResolver. The CacheResolver SPI lets you compute the target caches dynamically per invocation: resolveCaches(CacheOperationInvocationContext) returns a Collection<Cache>, and the context exposes the target, method, arguments, and the cache operation. That enables routing by tenant, argument, or environment — picking a different CacheManager or cache name at call time. You wire a custom resolver per operation with @Cacheable(cacheResolver="beanName"), or globally by implementing CachingConfigurer (registered because you use @EnableCaching) and overriding cacheResolver(). CachingConfigurer also lets you override cacheManager(), keyGenerator(), and errorHandler() in one place. In Spring 6 CachingConfigurer has default methods returning null, so CachingConfigurerSupport is deprecated — just implement the interface and override what you need.

code

java · 25 lines
java
public class TenantCacheResolver extends AbstractCacheResolver {
    public TenantCacheResolver(CacheManager cacheManager) { super(cacheManager); }

    @Override
    protected Collection<String> getCacheNames(CacheOperationInvocationContext<?> ctx) {
        String tenant = TenantContext.current();
        // one physical cache per declared name, namespaced by tenant
        return ctx.getOperation().getCacheNames().stream()
                .map(name -> name + "::" + tenant)
                .toList();
    }
}

@Configuration
@EnableCaching
class CacheConfig implements CachingConfigurer {
    @Bean CacheManager cacheManager() { return new CaffeineCacheManager(); }

    @Override public CacheResolver cacheResolver() {
        return new TenantCacheResolver(cacheManager());
    }
    @Override public CacheErrorHandler errorHandler() {
        return new SimpleCacheErrorHandler(); // or a resilient custom one
    }
}

go deeper

for a junior

Know the default resolver maps names to a CacheManager; a CacheResolver can customize this.

for a middle

Explain resolveCaches, the invocation context, and per-operation cacheResolver wiring.

for a senior

Implement dynamic routing (tenant/argument) and register it globally via CachingConfigurer.

for a principal

Design multi-tenant/multi-provider resolution with attention to per-call cost, precedence, and mutual exclusivity rules.

## Static vs dynamic cache resolution Out of the box, `@Cacheable("users")` resolves the cache **statically**: the framework takes the literal name(s) and looks them up in the configured `CacheManager`. The component doing this is a `CacheResolver` — specifically the default `SimpleCacheResolver`, which wraps a single `CacheManager` and returns `manager.getCache(name)` for each declared name. ## The CacheResolver SPI `org.springframework.cache.interceptor.CacheResolver` has one method: ``` Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context); ``` The `CacheOperationInvocationContext` gives you everything about the call: - `getTarget()` — the object being invoked, - `getMethod()` — the method, - `getArgs()` — the actual arguments, - `getOperation()` — the `CacheOperation` (its declared cache names, key expression, etc.). Because resolution runs **per invocation**, you can choose caches dynamically: e.g. pick a tenant-specific cache name, choose between two `CacheManager`s based on an argument, or return an empty collection to skip caching conditionally at the store level. `AbstractCacheResolver` is a convenient base — you override `getCacheNames(context)` and it looks them up in a `CacheManager` you provide. ## Scoping a resolver - **Per operation**: `@Cacheable(cacheResolver = "myResolver")` (mutually exclusive with `cacheManager` on the same annotation). - **Globally**: via `CachingConfigurer` (below). A per-operation `cacheResolver`/`cacheManager` overrides the global one. ## CachingConfigurer — the central customization hook When you enable caching with `@EnableCaching`, Spring looks for a bean implementing `org.springframework.cache.annotation.CachingConfigurer` to obtain shared caching infrastructure. Its methods (all with default implementations returning `null` in Spring 6+, so override only what you need): - `cacheManager()` — the default `CacheManager`. - `cacheResolver()` — a global `CacheResolver` (mutually exclusive with providing a `cacheManager`; if you supply a resolver, the manager is optional). - `keyGenerator()` — default `KeyGenerator` (key/keyGenerator specifics belong to the annotation-model leaf). - `errorHandler()` — a `CacheErrorHandler` for cache-store failures. Historically you extended `CachingConfigurerSupport`; since Spring 6 that class is **deprecated** because the interface now has default methods — implement `CachingConfigurer` directly. Only one `CachingConfigurer` should be present; multiple lead to ambiguity. ## Interaction and precedence - If an operation specifies neither `cacheManager` nor `cacheResolver`, the global `cacheResolver()` from `CachingConfigurer` is used; if that's null, Spring builds a `SimpleCacheResolver` around the global `cacheManager()`. - Per-operation attributes win over global config. - Providing both a global `cacheResolver` and a global `cacheManager` is redundant/ambiguous — pick one. ## Use cases - **Multi-tenancy**: resolve `orders::tenant42` at call time from a `TenantContext`. - **A/B or environment routing**: choose Redis vs local cache dynamically. - **Conditional store selection**: return an empty cache collection to bypass caching for certain args (distinct from the annotation's `condition`/`unless`, which are the annotation-model leaf's concern). ## Gotchas - A `CacheResolver` returning an empty collection means 'no cache' — the method just executes; make sure that's intended. - `cacheManager` and `cacheResolver` are mutually exclusive on the same `@Cacheable` and in `CachingConfigurer`. - Beware performance: `resolveCaches` runs on every cached call, so keep it cheap. - Custom resolvers must still return caches that the configured providers actually own.

  • How does a CacheResolver differ from the @Cacheable key/keyGenerator mechanism?
    CacheResolver picks which Cache instances (buckets) an operation uses at runtime; key/keyGenerator picks the entry key within a resolved cache. They operate at different levels and are configured independently.
  • Since Spring 6, how should you register global caching infrastructure?
    Implement CachingConfigurer directly and override only the methods you need (cacheManager, cacheResolver, keyGenerator, errorHandler). CachingConfigurerSupport is deprecated because the interface now has default methods.

saying these in an interview costs you the question

  • Confusing CacheResolver (which cache) with KeyGenerator (which key)
  • Setting both cacheManager and cacheResolver on the same @Cacheable (mutually exclusive)
  • Still extending the deprecated CachingConfigurerSupport in Spring 6
  • Assuming resolveCaches runs once at startup rather than per invocation

context