Spring Cache Interview Self‑Test: Full Deep Dive, Correct Usage & Performance Boost
This article provides a comprehensive interview self‑test on Spring Cache, covering annotation differences, AOP internals, SpEL expressions, key generation, Redis serialization, multi‑level caching, consistency strategies, and Caffeine eviction policies, complete with code examples, diagrams, and practical recommendations.
Interview Self‑Test Overview
The article is a self‑test guide for Spring Cache interview questions, offering reference answers and encouraging readers to review the original blog for a complete knowledge system before attempting the quiz.
Q1: Differences and Execution Timing of @Cacheable, @CachePut, @CacheEvict
These three annotations correspond to the three core cache operations: read, write, and delete. @Cacheable – checks the cache first; if a hit occurs the method is skipped, otherwise the method executes and the result is cached. Suitable for read‑heavy methods. @CachePut – always executes the method first, then writes the returned value into the cache. Suitable for update/create methods. @CacheEvict – by default deletes the cache after method execution; the beforeInvocation flag (default false) can make deletion happen before the method, useful when the method may throw an exception.
Key points: @Cacheable and @CachePut are mutually exclusive – if both are present on the same method, the method is executed and the result is written to the cache. @CacheEvict has the important beforeInvocation parameter that controls when deletion occurs.
Analogy: @Cacheable is like checking a menu before ordering, @CachePut is like updating the menu after a new dish is prepared, and @CacheEvict is like crossing out a dish that is no longer available.
Q2: Underlying Principle of Spring Cache (AOP Proxy + CacheInterceptor)
Spring Cache works through AOP proxies that intercept calls to methods annotated with cache annotations.
caller
│
│ cachedUserService.getUserById(1)
▼
┌──────────────────────────┐
│ Proxy object (JDK/CGLIB) │ ← Spring creates this
├──────────────────────────┤
│ CacheInterceptor │ ← AOP entry point
│ ├─ CacheOperationSource│ ← Parses annotations
│ └─ CacheAspectSupport │ ← Core logic
│ ├─ findCachedItem() ← Cache lookup
│ ├─ updateCachedItem() ← Cache write
│ └─ evictCachedItem() ← Cache delete
├──────────────────────────┤
│ CacheManager │ ← Manages actual cache containers
│ └─ Cache │ ← Underlying cache (e.g., Caffeine, Redis)
└──────────────────────────┘
│
▼
actual method: users.get(id)Core components:
@EnableCaching triggers registration of ProxyCachingConfiguration, creating CacheInterceptor and BeanFactoryCacheOperationSource.
CacheOperationSource parses cache annotations on first invocation via reflection and caches the parsed result (key: Class+Method).
CacheInterceptor implements MethodInterceptor and delegates to CacheAspectSupport.execute().
CacheAspectSupport.execute() orchestrates the full flow: handles @CacheEvict(beforeInvocation=true), cache lookup, condition evaluation, method execution, unless evaluation, cache write, and post‑execution eviction.
Only calls that go through the proxy trigger caching; internal calls (e.g., this.method()) bypass the proxy and therefore do not activate cache logic.
Q3: Why Internal Calls Within the Same Class Do Not Trigger Cache
Spring Cache relies on AOP proxy interception. When a method in the same class calls another cached method via this, the call is made on the original target object, not the proxy, so CacheInterceptor is never invoked.
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(int id) { ... }
// Internal call – bypasses proxy, cache ineffective
public User getUserViaInternal(int id) {
return this.getUserById(id); // 'this' is the raw object
}
}Solutions (ordered by recommendation):
Split the cached method into a separate service so that cross‑class calls go through the proxy (most recommended).
Inject the service into itself lazily ( @Autowired @Lazy private UserService self;) and call self.getUserById().
Expose the proxy ( @EnableAspectJAutoProxy(exposeProxy = true)) and use AopContext.currentProxy() to invoke the method.
Verification: add logging or a counter inside getUserById. If the method runs on every call, the cache is not hit; if it runs only on the first call, the cache works.
Q4: SpEL Expression Usage in Cache Annotations
SpEL (Spring Expression Language) is used for the key, condition, and unless attributes. #parameterName – value of a method argument (e.g., #id). #parameterName.property – property of an argument object (e.g., #user.id). #root.methodName – name of the invoked method. #root.target – the target object instance. #root.caches – list of caches used. #root.args[0] – first argument. #result – method return value (available only for @CachePut and unless).
Parsing example:
Expression Parsing Process Result
"#id" lookup variable 'id' argument value 1
"#user.id" lookup 'user' → .id user ID
"#root.methodName" lookup root → .methodName "getUserById"
"#result" lookup variable 'result' method return value
"#result == null" boolean evaluation true/falseThe parser BeanFactoryExpressionEvaluator caches parsed Expression objects (key = expression string + target class) to avoid repeated parsing.
Q5: Difference Between condition and unless
Both control whether caching occurs, but they differ in evaluation timing and accessibility of #result:
condition – evaluated **before** method execution; cannot access #result. If false, the cache logic is skipped entirely.
unless – evaluated **after** method execution; can access #result. If true, the result is **not** cached.
Typical combination:
@Cacheable(value = "users", key = "#id",
condition = "#id > 0", // cache only for positive IDs
unless = "#result == null") // do not cache null results
public User getUser(int id) { ... }Analogy: condition is an “entry ticket” – if you don’t have it, you never enter the cache; unless is a “return policy” – after you get the result, you may decide not to store it.
Q6: Why @CacheEvict(allEntries=true) Is an Anti‑Pattern
Setting allEntries=true clears the entire cache namespace, which has several drawbacks:
Impact scope is too large – updating a single user removes cached data for all users, causing many cache misses.
Cache avalanche risk – simultaneous eviction can flood the database with requests, potentially causing overload.
Performance penalty – Redis may need to execute FLUSH or iterate over all keys, blocking its single‑threaded event loop.
Wasted cache value – the primary purpose of caching (reducing DB load) is lost until the cache is repopulated.
Recommended usage: delete only the specific key unless you truly need a full refresh (e.g., system configuration changes, scheduled cache warm‑up).
Analogy: clearing all entries is like discarding an entire phone book to update one contact; precise key deletion is like editing just that line.
Q7: Default Cache Key Generation (SimpleKeyGenerator)
If no key attribute is provided, Spring uses SimpleKeyGenerator:
0 parameters → SimpleKey.EMPTY (empty key).
1 parameter → the parameter value itself.
Multiple parameters → new SimpleKey(args...), which combines all arguments.
Code examples:
// Single parameter – key is the parameter value
@Cacheable(value = "users")
public User getUserById(int id) { ... } // key = 1
// Multiple parameters – combined into SimpleKey
@Cacheable(value = "products")
public Product getProduct(String category, int productId) { ... } // key = SimpleKey["electronics",100]
// No parameters – empty key
@Cacheable(value = "configs")
public Map<String,Object> getAllConfigs() { ... } // key = SimpleKey.EMPTYNote: default strategy can cause key collisions when different methods have identical signatures without explicit keys. It is recommended to always specify a key, especially when multiple cache methods share the same CacheManager. Custom key generators can be implemented by providing a KeyGenerator bean.
Q8: Choosing Redis Serialization Strategy (JSON vs JDK)
Both key and value need serializers. Common choices: GenericJackson2JsonRedisSerializer – JSON, human‑readable, good performance, cross‑language compatible. Recommended for production. JdkSerializationRedisSerializer – binary, not readable, average performance, Java‑only. Not recommended. StringRedisSerializer – for plain strings, best performance for keys.
Recommended configuration:
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> keySerializer = new StringRedisSerializer();
RedisSerializer<Object> valueSerializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
.disableCachingNullValues();
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
}Additional suggestions: disable caching of null values to avoid cache pollution, and set reasonable TTLs with entryTtl().
Q9: Multi‑Level Cache Design (L1 Caffeine + L2 Redis)
Combines the nanosecond‑level speed of local Caffeine cache (L1) with the distributed sharing of Redis (L2).
Request → L1 Caffeine (nanoseconds)
├─ Hit → return
└─ Miss → L2 Redis (milliseconds)
├─ Hit → write to L1, return
└─ Miss → query DB → write to L2 → write to L1 → returnWhy needed:
Pure Redis incurs network latency on every access, becoming a bottleneck under high concurrency.
Pure Caffeine lacks data consistency across multiple JVM instances.
Multi‑level cache offers fast local reads while keeping data consistent across services.
Implementation highlights:
Wrap a CacheManager that delegates get/put/evict operations to both L1 and L2.
Typical TTL: short (e.g., 1 min) for L1, longer (e.g., 10 min) for L2.
Consistency flow: update DB → evict L2 → evict L1, ensuring eventual consistency.
Analogy: similar to CPU cache hierarchy (L1/L2/L3), where each level is consulted only when the previous level misses.
Q10: Common Cache Consistency Schemes
Four typical approaches:
Cache Aside – update DB first, then delete the related cache entry. Low complexity, eventual consistency. Recommended for most cases.
Delayed Double Delete – delete cache, update DB, then delete cache again after a short delay to clean up any stale writes that may have occurred between the first delete and DB update. Provides near‑strong consistency, medium complexity.
Binlog Subscription – capture database change events (MySQL binlog → Canal/Debezium → message queue) and asynchronously delete or update cache entries. High complexity, suitable for micro‑service architectures.
Read‑Write Lock – use distributed locks to guarantee strong consistency during updates. High complexity, used when strict consistency is required.
Code snippets:
// Cache Aside (standard)
@CacheEvict(value = "users", key = "#id")
public void updateUser(int id, String name) {
userRepository.updateName(id, name); // update DB first
// @CacheEvict automatically removes the stale entry
}
// Delayed double delete
public void updateUser(int id, String name) {
cacheManager.getCache("users").evict(id); // 1. delete cache
userRepository.updateName(id, name); // 2. update DB
CompletableFuture.runAsync(() -> {
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
cacheManager.getCache("users").evict(id); // 3. delete again after delay
});
}
// Binlog subscription (conceptual)
MySQL → binlog → Canal/Debezium → MQ → Consumer → delete cacheAnalogy: Cache Aside is like “update the contract then tear off the copy”; Delayed Double Delete is “tear off the copy, update the contract, wait a moment, then tear off the copy again”; Binlog subscription is “the notary office notifies everyone to discard old copies”.
Q11: Why Cache Annotations on Private Methods Do Not Work
Two reasons:
AOP proxy limitation – Spring creates proxies that can only intercept public methods. Private methods are invisible to both JDK dynamic proxies and CGLIB subclasses.
CacheOperationSource scanning rule – the source parser only scans public methods; annotations on private/protected methods are ignored.
Solution: make the method public or move the caching logic to a separate service class that is called via the proxy.
// Separate service for caching
@Service
public class UserCacheService {
@Cacheable(value = "users", key = "#id")
public User getUserById(int id) { ... }
}Analogy: the proxy is like a building’s front‑door security guard who only checks visitors entering through the main entrance (public methods); private back‑door corridors are not monitored.
Q12: Caffeine Eviction Strategies
Caffeine uses the W‑TinyLFU algorithm (a hybrid of a small LRU window and a main cache based on TinyLFU frequency). maximumSize=N – approximate LRU, limits total entries. maximumWeight=N – weight‑based eviction using a custom weigher function. expireAfterWrite – entry expires N time units after being written. expireAfterAccess – entry expires N time units after last access. refreshAfterWrite – asynchronously refreshes entry after N time units.
Configuration example:
@Bean
public CaffeineCacheManager cacheManager() {
CaffeineCacheManager manager = new CaffeineCacheManager();
manager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500) // at most 500 entries
.expireAfterWrite(Duration.ofMinutes(10)) // 10‑minute TTL
.recordStats()); // enable statistics
return manager;
}
# application.properties
spring.cache.type=caffeine
spring.cache.caffeine.spec=maximumSize=500,expireAfterWrite=10m,recordStatsW‑TinyLFU combines a small “window” cache (LRU) for recent accesses with a larger “main” cache that evicts based on frequency, achieving higher hit rates than pure LRU, especially when access patterns shift.
Comparison with Redis:
Location: JVM heap vs separate process/network.
Latency: nanoseconds vs milliseconds.
Data sharing: local only vs distributed.
Eviction: W‑TinyLFU vs configurable LRU/LFU/TTL.
Analogy: Caffeine is like sticky notes on a desk—fast to grab but limited in number; Redis is like a filing cabinet—large capacity but requires walking over.
Reference Materials
Java Development Guide – Spring Cache full analysis (source blog).
Spring official documentation: Caching.
Spring Boot auto‑configuration: CacheAutoConfiguration.
Spring source code: CacheAspectSupport, CacheInterceptor, BeanFactoryExpressionEvaluator.
Caffeine documentation: github.com/ben‑manes/caffeine.
"Spring in Action" Chapter 14 – Caching.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
CodeSmart Hoops
A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
