Intermediate · Accelerate
Understand What Spring Cache Does—and Does Not Do
Trace annotation interception, key resolution, provider calls, and proxy limitations.
Primary source: Spring Boot 3.5 Reference: Caching
Lesson 1 of 4
1d streakThe Architectural Question
@Cacheable looks declarative until a self-invocation skips the proxy and the "cached" method loads on every single request.
What this guide proves
- Choose an explicit cache boundary
- Use stable collision-resistant keys
- Remember that self-invocation may bypass caching proxies
The Spring Cache Abstraction and Proxy Boundary
The Spring Cache abstraction is not an in-memory cache implementation; it is a declarative, AOP-driven facade over third-party cache providers. When you annotate a method with @Cacheable, Spring dynamically wraps the enclosing bean in a CGLIB or JDK dynamic proxy that intercepts method execution.
Before executing the method body, the caching aspect computes a cache key, queries the configured org.springframework.cache.CacheManager, and returns the cached value immediately on a hit. The actual method body executes only when a cache miss occurs, at which point the returned value is automatically placed into the cache before returning to the caller.
package com.example.catalog;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class ProductCatalogService {
private final ProductRepository repository;
public ProductCatalogService(ProductRepository repository) {
this.repository = repository;
}
// Intercepted by CGLIB proxy: if key exists in "products", method body is skipped
@Cacheable(cacheNames = "products", key = "#sku", unless = "#result == null")
public Optional<Product> findBySku(String sku) {
// Only executes on cache miss
return repository.findBySku(sku);
}
// Always executes the method body, then updates the cache entry with the result
@CachePut(cacheNames = "products", key = "#product.sku()")
public Product updateProduct(Product product) {
return repository.save(product);
}
// Evicts the entry from "products" when product is deleted or archived
@CacheEvict(cacheNames = "products", key = "#sku")
public void deleteProduct(String sku) {
repository.deleteBySku(sku);
}
}Difference Between @Cacheable and @CachePut
SpEL Key Resolution, Collision Defense, and Namespaces
By default, Spring utilizes SimpleKeyGenerator to generate keys. If a method takes zero parameters, it returns SimpleKey.EMPTY. If it takes one parameter, it returns that parameter directly. If it takes multiple parameters, it wraps them into a composite SimpleKey object.
In production environments, relying on default key generation across multiple entities is dangerous. A User entity with ID 42 and a Product entity with ID 42 will collide if both use default keys in un-namespaced caches. Always specify explicit SpEL keys or customized KeyGenerator beans.
| SpEL Variable | Description | Example Expression |
|---|---|---|
| #root.methodName | Name of the intercepted method | key = "#root.methodName + ':' + #id" |
| #root.args[n] | Argument by positional index | key = "#root.args[0]" |
| #paramName | Method parameter by name (requires -parameters flag) | key = "'sku:' + #sku.toLowerCase()" |
| #result | The return value of the method (only in @CachePut/unless) | unless = "#result == null || #result.isDraft()" |
| #root.caches[0].name | The target cache container name | key = "#root.caches[0].name + ':' + #id" |
package com.example.catalog;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component("prefixedKeyGenerator")
public class PrefixedKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
// Enforce safe namespace prefixing: TargetClass:MethodName:arg1:arg2
return target.getClass().getSimpleName() + ":" + method.getName() + ":" + Arrays.deepToString(params);
}
}Compile with the -parameters Compiler Flag
The Self-Invocation Bypass Trap and Proxy Resolution
Because Spring caching relies on AOP proxies by default (mode = AdviceMode.PROXY), calling a @Cacheable method from another method inside the very same class completely bypasses the caching proxy. The invocation is dispatched directly on the internal this reference, skipping the cache interception aspect entirely.
The Silent Self-Invocation Failure
To resolve self-invocation without breaking architectural layering, extract the cached operation into a dedicated collaborator bean, or inject a self-reference via setter/provider:
package com.example.catalog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class CatalogLookupService {
private CatalogLookupService self;
// Self-injection via @Lazy setter to route internal calls through the CGLIB proxy
@Autowired
public void setSelf(@Lazy CatalogLookupService self) {
this.self = self;
}
public Optional<Product> getOrHydrate(String sku) {
// Calling self.findBySku routes through proxy and hits cache!
return self.findBySku(sku);
}
@Cacheable(cacheNames = "products", key = "#sku")
public Optional<Product> findBySku(String sku) {
return Optional.of(new Product(sku, "Widget", 1999));
}
}Validation, Hit-Rate Metrics, and Verification Checkpoints
Verify caching behavior by measuring hit rates and invocation counters using Spring Boot Actuator metrics before promoting to production:
# 1. First request triggers cache miss and populates cache
curl -i http://localhost:8080/api/products/SKU-42
# HTTP/1.1 200 OK -> Database log: SELECT * FROM products WHERE sku = 'SKU-42'
# 2. Second request hits the cache instantly (no DB query executed)
curl -i http://localhost:8080/api/products/SKU-42
# 3. Query cache metrics via Actuator
curl -s http://localhost:8080/actuator/metrics/cache.gets?tag=cache:products
# Verify {"name":"cache.gets","measurements":[{"statistic":"COUNT","value":2.0}]}Active Recall Checkpoint
Why does calling this.find(id) fail to hit the cache even if find() is annotated with @Cacheable?
Hint: Consider how Spring AOP wraps beans in CGLIB subclasses.
Show answer & explanation
Spring Cache uses dynamic proxies. Calling a method on "this" invokes the target object directly without passing through the interceptor proxy.
Runtime Architecture & Flow
Persistence context · runtime visualization
Understand What Spring Cache Does—and Does Not Do
Transaction
Choose an explicit cache boundary
Entity state
Use stable collision-resistant keys
Flush
Remember that self-invocation may bypass caching proxies
Commit
Cache a product-view method and verify hit and miss behavior.
Working Implementation
Code Walkthrough
Cache a product-view method and verify hit and miss behavior.
package com.example.cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Service
public class CacheAbstractionLab {
private final Map<String, Product> database = new ConcurrentHashMap<>();
private final AtomicInteger databaseQueries = new AtomicInteger();
private CacheAbstractionLab self;
// Self-injection via @Lazy setter to route internal calls through the CGLIB proxy
@Autowired
public void setSelf(@Lazy CacheAbstractionLab self) {
this.self = self;
}
public record Product(String sku, String name, int priceCents) {}
// First call executes method body; subsequent calls return cached value
@Cacheable(cacheNames = "catalog", key = "#sku", unless = "#result == null")
public Optional<Product> findBySku(String sku) {
databaseQueries.incrementAndGet();
return Optional.ofNullable(database.get(sku));
}
// Calling self.findBySku routes through proxy and hits cache; this.findBySku would bypass proxy!
public Optional<Product> getThroughProxy(String sku) {
return self.findBySku(sku);
}
@CacheEvict(cacheNames = "catalog", key = "#sku")
public void evict(String sku) {
database.remove(sku);
}
public void seed(Product product) {
database.put(product.sku(), product);
}
public int getDatabaseQueryCount() {
return databaseQueries.get();
}
}Synthesis & Production Verification
Production Acceptance Criteria
- Trace one call from interception through key generation to the provider
- Design keys that are stable, collision-resistant, and namespace-safe
- Demonstrate the self-invocation bypass and its repair
Key Takeaway
Spring Cache provides declarative AOP method interception; calling cached methods internally bypasses the CGLIB proxy, and keys must be explicitly namespaced to prevent cross-entity collisions.
Architectural & Technical Review
Core Interview Questions
Q01How does Spring Cache intercept method execution and under what conditions does it skip the method body?Show answer
Spring wraps the bean in a CGLIB or JDK dynamic proxy. When a caller invokes a @Cacheable method, the proxy intercepts the call, computes the key, and queries the CacheManager. If a cached value is present, the proxy returns it immediately and skips the method body.
Q02Why is relying on default SimpleKeyGenerator dangerous in multi-entity production systems?Show answer
SimpleKeyGenerator computes keys solely based on method arguments without entity namespaces. If User and Product services both look up ID 42, their keys collide in shared caches. Explicit SpEL keys or namespaced KeyGenerators are required.
Q03Why does calling this.findProduct(sku) bypass @Cacheable, and how do you resolve it without breaking architectural layering?Show answer
Direct calls via "this" invoke the target object directly without passing through the CGLIB proxy interceptor. This can be resolved by extracting the cached method into a collaborator bean or by injecting a self-reference via a @Lazy setter.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Invoking cached methods on this reference inside the same bean — Bypasses the CGLIB proxy entirely, executing the underlying database query on every request without caching.
- Pitfall: Omitting -parameters compiler flag when using SpEL argument keys — Causes parameter names to be erased from bytecode, resulting in SpEL evaluation exceptions at runtime.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources