Spring Boot

Caching in Spring Boot

Learn how Spring caching works with @EnableCaching, @Cacheable, @CacheEvict, @CachePut, cache keys, and distributed cache concerns.

Spring BootCachingRedisPerformanceAnnotations

The Short Version

Spring caching lets you store method results so repeated calls with the same inputs can return quickly without re-running expensive logic.

The most common setup is: enable caching with @EnableCaching, add @Cacheable to read methods, and use @CacheEvict or @CachePut when data changes.

Step 1: Enable Caching

First, enable Spring's caching abstraction in a configuration class or on your main application class.

java
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableCaching
public class CacheConfig {
}

This does not force a specific cache technology. Spring provides a caching abstraction, and the actual cache provider can be simple in-memory caching, Caffeine, Valkey, Redis, Ehcache, or another provider.

Step 2: Cache a Read Method

Use @Cacheable when a method is expensive and returns the same result for the same input.

java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Cacheable(value = "products", key = "#id")
    public Product getProductById(Long id) {
        System.out.println("Calling database...");
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }
}

The first call for a product id hits the database. Later calls with the same id can return from the cache.

What @Cacheable Means

  • value = "products" names the cache.
  • key = "#id" tells Spring to use the method argument as the cache key.
  • If the key exists in the cache, the method body is skipped.
  • If the key does not exist, the method runs and the result is stored.

Evict Cache When Data Changes

Caching reads is easy. The harder part is keeping cached data correct when writes happen.

java
import org.springframework.cache.annotation.CacheEvict;

@CacheEvict(value = "products", key = "#id")
public Product updateProduct(Long id, ProductRequest request) {
    Product product = productRepository.findById(id)
        .orElseThrow(() -> new ProductNotFoundException(id));

    product.setName(request.name());
    product.setPrice(request.price());

    return productRepository.save(product);
}

After the product is updated, the old cached value is removed. The next read will hit the database and cache the fresh value.

Observe that the same cache (named product) is referenced in the getProductById and updateProduct methods.

Evict All Entries

Sometimes one write affects many cached entries. In that case, you can clear the whole cache.

java
@CacheEvict(value = "products", allEntries = true)
public void bulkUpdateProducts(List<ProductRequest> requests) {
    // update many products
}
Use allEntries = true carefully. It is simple and safe, but it can cause many future requests to miss the cache at once.

Use @CachePut When You Want to Update the Cache

@CachePut always runs the method and stores the returned value in the cache.

java
import org.springframework.cache.annotation.CachePut;

@CachePut(value = "products", key = "#id")
public Product updateProductAndCache(Long id, ProductRequest request) {
    Product product = productRepository.findById(id)
        .orElseThrow(() -> new ProductNotFoundException(id));

    product.setName(request.name());
    product.setPrice(request.price());

    return productRepository.save(product);
}

The difference is important: @Cacheable may skip the method, but @CachePut always executes it.

Conditional Caching

Spring also lets you cache only under certain conditions.

java
@Cacheable(
    value = "products",
    key = "#id",
    condition = "#id != null",
    unless = "#result == null"
)
public Product getProductById(Long id) {
    return productRepository.findById(id).orElse(null);
}
  • condition is checked before the method runs.
  • unless is checked after the method runs.
  • unless = "#result == null" avoids caching null results.

Caching Does Not Understand Business Consistency

One important interview point is that Spring caching does not magically understand your business consistency rules.

Spring only follows the cache annotations you configure. It does not know which pieces of data become stale after a write operation.

That means you must decide:

  • when cached data should be evicted
  • when cached data should be updated
  • which cache keys are affected by a write
  • whether a single entry or the whole cache should be cleared

For example, updating one product may affect:

  • the product-by-id cache
  • a product search cache
  • a category listing cache
  • a pricing cache

If only one cache entry is evicted while other related caches remain stale, users may receive inconsistent data.

This is one reason caching becomes difficult in real systems: performance improves, but consistency becomes harder to maintain.

Important Interview Follow-Ups

What is cached?

Usually method return values, based on the method arguments and cache key.

What is the key?

By default Spring builds a key from method arguments, but explicit keys are clearer in interviews.

What about stale data?

Cached values can become outdated if writes do not evict or update the cache.

Is this distributed?

Not automatically. A local in-memory cache is per application instance.

When use Redis?

Use Redis or another external cache when multiple app instances need shared cache state.

What about TTL?

TTL is usually configured in the cache provider, not directly on @Cacheable.

Local Cache vs Distributed Cache

A local cache is simple and fast, but each application instance has its own copy.

In a cluster with several Spring Boot instances, one server might evict or update its local cache while another server still has stale data.

A distributed cache such as Redis helps because all application instances use the same shared cache.

This is one of the most important senior-level caching points: caching improves speed, but it introduces consistency problems.

Final Takeaway

For interviews, explain the full lifecycle: enable caching, cache expensive reads, evict or update cached data on writes, and discuss stale data, TTL, and distributed cache concerns.