Skip to content

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.


  • 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):
    1. 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.
    2. 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”
TypeAnalogyDescriptionProsCons
In-MemoryEach 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.
DistributedThere 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 infrastructure
public class MyAppApplication { ... }
  • 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:

@Service
public 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 key 123.
  • 2nd Call: findProductById(123) -> The cache is checked. A value is found for key 123. The cached DTO is returned immediately. The method’s code is never executed.
  • 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:

@Service
public 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.

  • 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:

@Service
public 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”
  1. Q: What is the default caching provider in Spring Boot if you just use @EnableCaching and 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.”
  2. 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 SimpleKeyGenerator will use the method parameter, in this case, the email string, as the key. This works well. However, if the method had multiple parameters, say (String email, boolean isActive), the default key would be a SimpleKey object 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’s hashCode() and equals() 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 the key attribute, like @Cacheable(key = "#email").”
  3. Q: What is the critical difference between @Cacheable and @CachePut? When would you use one over the other?

    • A: “The critical difference is that @Cacheable is conditional, while @CachePut is unconditional. @Cacheable first checks the cache and will skip the method execution if a value is found. @CachePut never skips the method execution; its primary purpose is to update the cache with the new result of the method. You use @Cacheable on read/find methods to improve performance by avoiding redundant work. You use @CachePut on update/edit methods to ensure that your cache is populated with the latest data after a modification.”
  4. Q: A method annotated with @Cacheable is 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 this reference, 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.”
  5. Q: Can you apply @Cacheable to 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.”