Basic of Caching in Spring Boot JPA
The Professional’s Guide to Spring Boot Caching
Section titled “The Professional’s Guide to Spring Boot Caching”Objective: To master the Spring Cache Abstraction, enabling the implementation of robust, performant caching strategies and to confidently articulate the design patterns and trade-offs in a technical interview.
1.1 What is Caching? The “Why”
Section titled “1.1 What is Caching? The “Why””- Definition: Caching is the practice of storing a copy of frequently accessed data in a temporary, high-speed storage location (the “cache”) to serve future requests faster.
- Analogy: Imagine a library. The database is the main archive in the basement, which is slow to access. The cache is a small shelf of the most popular books kept right behind the front desk. When someone asks for a popular book, the librarian gives them the copy from the front desk shelf, avoiding a slow trip to the archive.
- Why it’s Needed (The Business Case):
- Performance: Reading from memory (cache) is orders of magnitude faster than reading from a database over a network. This directly translates to lower API response times.
- Scalability & Cost Reduction: By serving a significant portion of requests from the cache, you dramatically reduce the load on your primary database, allowing it to serve more critical write operations and potentially reducing your database hosting costs.
1.2 Types of Caching: In-Memory vs. Distributed
Section titled “1.2 Types of Caching: In-Memory vs. Distributed”| Type | Analogy | Description | Pros | Cons |
|---|---|---|---|---|
| In-Memory | Each librarian has their own personal shelf. | The cache lives in the same memory space (JVM heap) as the application instance. | Extremely fast. No network overhead. Simple to set up. | Cache is lost on restart. Not shared between multiple instances of the same application. |
| Distributed | There is one central, shared shelf that all librarians use. | The cache is a separate, external service (like Redis or Hazelcast) that all application instances connect to over the network. | Shared and consistent across all application instances. Survives application restarts. | Slower than in-memory due to network calls. More complex to set up and manage. |
Part 2: Spring’s Cache Abstraction - The Core Annotations
Section titled “Part 2: Spring’s Cache Abstraction - The Core Annotations”Spring provides a brilliant abstraction layer for caching. You use a set of simple annotations, and the underlying caching implementation (in-memory, Redis, etc.) can be swapped out with configuration changes, not code changes.
The First Step: Enabling Caching
You must add @EnableCaching to your main application class or a @Configuration class. This is the “on-switch” that tells Spring to look for caching annotations.
@SpringBootApplication@EnableCaching // Activate Spring's caching infrastructurepublic class MyAppApplication { ... }@Cacheable: The Read-Through Cache
Section titled “@Cacheable: The Read-Through Cache”- What it is: This is the most common annotation. It tells Spring, “Before executing this method, check the cache. If a result is found for the given arguments, return it directly without running the method. If not, run the method, put the result in the cache, and then return it.”
- Analogy: “Check the front desk shelf for this book first. If it’s not there, go to the archive, get the book, put a copy on the front desk shelf, and then give it to the customer.”
- Key Concept: The Cache Key: How does Spring know if the “result for the given arguments” is in the cache? It generates a key. By default, it uses a hash of all the method’s parameters.
Code Example:
@Servicepublic class ProductService {
// The 'cacheNames' attribute defines which "shelf" to use. // The key will be the 'productId' parameter by default. @Cacheable(cacheNames = "products", key = "#productId") public ProductDTO findProductById(Long productId) { // This log and the database call will ONLY happen if the product // is not already in the 'products' cache with this ID. log.info("Fetching product {} from the database...", productId); return productRepository.findById(productId) .map(this::convertToDto) .orElseThrow(...); }}- 1st Call:
findProductById(123)-> Method executes, logs message, hits DB, returns DTO. The result is stored in the “products” cache with key123. - 2nd Call:
findProductById(123)-> The cache is checked. A value is found for key123. The cached DTO is returned immediately. The method’s code is never executed.
@CachePut: The Unconditional Update
Section titled “@CachePut: The Unconditional Update”- What it is: This annotation always executes the method and then updates the cache with the method’s result.
- Analogy: “A new edition of a book has arrived. Go get it from the archive, and unconditionally replace the old copy on the front desk shelf with this new one.”
- Why it’s used: To update a cached entry without interfering with the method’s execution. This is typically used on update/edit methods.
Code Example:
@Servicepublic class ProductService {
@CachePut(cacheNames = "products", key = "#productId") public ProductDTO updateProduct(Long productId, ProductUpdateDTO updateDto) { log.info("Updating product {} in the database...", productId); Product product = productRepository.findById(productId).orElseThrow(...); // ... apply updates from dto ... Product updatedProduct = productRepository.save(product); return convertToDto(updatedProduct); }}When updateProduct(123, ...) is called, the method will always run, the database will be updated, and the new ProductDTO returned by the method will replace the old value in the “products” cache for key 123. This ensures the cache does not become stale.
@CacheEvict: Removing Entries
Section titled “@CacheEvict: Removing Entries”- What it is: This annotation is used to remove an entry from the cache.
- Analogy: “Someone reported an error in a book. Remove the copy from the front desk shelf immediately. The next person who asks for it will get a fresh copy from the archive.”
- Why it’s used: Typically placed on delete operations to ensure that deleted data is removed from the cache.
Code Example:
@Servicepublic class ProductService {
@CacheEvict(cacheNames = "products", key = "#productId") public void deleteProduct(Long productId) { log.info("Deleting product {} from the database...", productId); productRepository.deleteById(productId); }
// You can also evict all entries from a cache @CacheEvict(cacheNames = "products", allEntries = true) public void refreshAllProducts() { // This method might be called by a scheduler to clear the cache periodically. }}Interview Gold: The Nuances of the Cache Abstraction
Section titled “Interview Gold: The Nuances of the Cache Abstraction”-
Q: What is the default caching provider in Spring Boot if you just use
@EnableCachingand nothing else?- A: “The default provider is a simple
ConcurrentHashMap-based cache (ConcurrentMapCache). This is an in-memory, non-persistent cache. It’s excellent for development, testing, or simple single-instance applications, but it is not suitable for a multi-node production environment as the caches would be inconsistent across instances.”
- A: “The default provider is a simple
-
Q: You have a method
@Cacheable public User findByEmail(String email). What is the default cache key Spring will generate? What is a potential problem with this?- A: “By default, Spring’s
SimpleKeyGeneratorwill use the method parameter, in this case, theemailstring, as the key. This works well. However, if the method had multiple parameters, say(String email, boolean isActive), the default key would be aSimpleKeyobject composed of both parameters. A problem arises if you have a method with a complex, non-primitive object as a parameter. The default key generation relies on the object’shashCode()andequals()methods. If those methods are not implemented correctly, you can get unexpected cache misses or collisions. That’s why it’s often a best practice to be explicit and define the key yourself using thekeyattribute, like@Cacheable(key = "#email").”
- A: “By default, Spring’s
-
Q: What is the critical difference between
@Cacheableand@CachePut? When would you use one over the other?- A: “The critical difference is that
@Cacheableis conditional, while@CachePutis unconditional.@Cacheablefirst checks the cache and will skip the method execution if a value is found.@CachePutnever skips the method execution; its primary purpose is to update the cache with the new result of the method. You use@Cacheableon read/find methods to improve performance by avoiding redundant work. You use@CachePuton update/edit methods to ensure that your cache is populated with the latest data after a modification.”
- A: “The critical difference is that
-
Q: A method annotated with
@Cacheableis being called from another method within the same class. The caching is not working. Why?- A: “This is the classic self-invocation problem, just like in AOP. Spring’s caching is implemented using proxies. When you call a method on the
thisreference, you are calling the method on the raw target object, completely bypassing the caching proxy. Therefore, the caching logic never runs. To solve this, the call must be made from a different bean, or you would need to inject the bean into itself to get a reference to the proxy.”
- A: “This is the classic self-invocation problem, just like in AOP. Spring’s caching is implemented using proxies. When you call a method on the
-
Q: Can you apply
@Cacheableto a private method?- A: “No. The caching annotations will not work on private or protected methods. Because Spring’s caching relies on AOP proxies, which typically use subclassing (CGLIB) or interface implementation (JDK Proxies), the method must be public and non-final to be intercepted by the proxy.”