Spring Boot Caching: @Cacheable, @CacheEvict, and Redis

Implement Spring Boot caching with @Cacheable, @CacheEvict, and Redis: reduce database load, handle cache hits, misses, and invalidation properly.

published: reading time: 26 min read author: GeekWorkBench
Quick Summary

Implement Spring Boot caching with @Cacheable, @CacheEvict, and Redis: reduce database load, handle cache hits, misses, and invalidation properly. The guide uses practical examples to explain introduction to spring boot caching, when to use and when to avoid caching and shows how to apply the ideas in a Spring Boot project.

Spring Boot Caching: @Cacheable, @CacheEvict, and Redis

Caching reduces database load and cuts response times significantly. Spring Boot makes caching accessible through its annotation model, but that simplicity hides depth. Get the fundamentals wrong and you will spend weeks chasing stale data bugs and production outages.

This guide walks through Spring Boot caching from the ground up. You will learn when caching genuinely helps versus when it adds complexity you do not need, see the annotated patterns that matter in practice, understand the failure modes that send engineers to production at 2 AM, and pick up the observability habits that separate teams who debug cache issues quickly from those who cannot. If you are working through the Spring Boot learning path, this post fits between the fundamentals and the advanced microservices patterns.

Introduction to Spring Boot Caching

Introduction

Spring Boot caching stores frequently requested results so applications can reduce database work and respond faster. This guide explains how @Cacheable, @CacheEvict, and related annotations behave, when Redis is appropriate, and how to reason about cache misses, invalidation, consistency, and failure scenarios.

When to Use and When to Avoid Caching

Caching earns its place when three conditions hold simultaneously: the same data gets requested frequently, the computation or database query behind it is expensive, and the data changes infrequently relative to how often it gets read. User profiles, product catalogs, configuration lookups, and reference data all fit this pattern naturally. The moment you apply caching to data that changes every few seconds, you inherit a stale-data problem that can silently corrupt user experiences in ways that are devilish to reproduce and diagnose.

What people underestimate is cache invalidation. Two strategies compete. Time-based expiration (TTL) is simple to implement and guarantees eventual consistency, but it trades freshness for staleness. Active invalidation through @CacheEvict gives you point-in-time correctness, but it requires you to identify every code path that modifies data and wire the eviction correctly. Miss a single update path and you serve stale data until the TTL expires. Caching shines brightest with read-heavy data that has clear, enumerated write paths. It becomes a liability when writes are frequent and scattered across many services.

Skip caching when your application is write-heavy with short retention windows, when data must always reflect the absolute latest state (financial transactions, inventory counts), or when your cache infrastructure would cost more to operate than simply scaling the database. Only cache when you have measured that the performance gain justifies the consistency tradeoff.

graph TD
    A[Request arrives] --> B{Cache lookup}
    B -->|Hit| C[Return cached value]
    B -->|Miss| D[Execute method]
    D --> E[Store result in cache]
    E --> F[Return result]
    C --> G[(Cache)]
    F --> G
    H[Write operation] --> I{Cache eviction}
    I --> J[Remove from cache]
    J --> G

Failure Scenarios

Stale Data

Stale data occurs when the cache reflects a state that no longer matches the database. This happens whenever data changes without a corresponding eviction. Imagine a user updates their email address. If the profile service writes to the database but your caching layer does not receive the eviction event, the next read returns the old email. The result ranges from minor annoyance to serious business impact (incorrect reports, compliance issues). Mitigations include active invalidation on all write paths, reasonable TTL values as a safety net, and monitoring that alerts when cache hit rates spike unexpectedly.

Cache Penetration

Cache penetration happens when requests repeatedly query for non-existent data. Each request bypasses the cache because the key does not exist, then hits the database. Enough of these arriving simultaneously can overwhelm your database with queries for keys that will never match. The standard defense is to cache negative results. When a database query returns null, store a sentinel value in the cache with a short TTL. Repeated queries for the same non-existent key return quickly from the cache instead of hammering the database.

Cache Avalanche

Cache avalanche occurs when a large number of keys expire simultaneously and a spike of requests all miss the cache at once, flooding the backend. This commonly happens when you set identical TTL values on many entries during a deployment or when a cache node restarts and its contents are lost. Suddenly thousands of requests call the database simultaneously because none of them find a cached value. Preventing this requires randomizing TTL values so keys do not expire in lockstep, pre-warming the cache after restarts, and implementing request coalescing so that only one request per key triggers the backend fetch.

Trade-off Table: In-Memory vs Redis vs Memcached

Feature In-Memory (Caffeine/Guava) Redis Memcached
Latency Sub-microsecond, no network 1-5ms network round-trip 1-5ms network round-trip
Capacity Limited to JVM heap Scales to tens of TB Scales to tens of GB per node
Persistence None (lost on restart) Optional RDB/AOF persistence None (lost on restart)
Clustering Not native (sticky sessions) Native clustering, replication Shared-nothing cluster model
Serialization JVM objects (no conversion) Requires JSON/byte serialization Requires byte serialization
Use Case Single-instance apps, local caching Distributed microservices, shared state Simple distributed blob cache
TTL Support Yes Yes Yes
Atomic Operations No Yes (INCR, SETNX, etc.) Limited
Setup Complexity Minimal Medium Low

For a single-instance Spring Boot service with modest cache needs, in-memory caching with Caffeine is the simplest path. Once you scale horizontally across multiple application instances, Redis becomes essential because it provides a shared, consistent view of cached data across all nodes. Memcached works for simple distributed blob caching, but Redis has taken over most of its use cases in Spring Boot ecosystems due to richer data structures and better tooling. For a full overview of distributed systems patterns including caching strategies, see the distributed systems roadmap.

Implementation

Enabling Caching

The entry point is @EnableCaching on any @Configuration class. This activates Spring’s CachingInterceptor and registers the cache advisors that power the annotations.

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

@Configuration
@EnableCaching
public class CacheConfig {
}

Basic @Cacheable

The @Cacheable annotation marks methods whose results should be cached. By default, the cache name comes from the value or cacheNames attribute, and the cache key is derived from all method arguments.

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

@Service
public class ProductService {

    @Cacheable(cacheNames = "products", key = "#productId")
    public Product getProduct(Long productId) {
        System.out.println("Fetching product from database: " + productId);
        return productRepository.findById(productId).orElseThrow();
    }
}

Cache Eviction

@CacheEvict removes entries when data changes. Use allEntries = true to clear the entire cache, or specify a key to remove a single entry.

import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @CacheEvict(cacheNames = "products", allEntries = true)
    public void refreshAllProducts() {
        // This clears the entire "products" cache
    }

    @CacheEvict(cacheNames = "products", key = "#productId")
    public void updateProduct(Long productId, Product product) {
        productRepository.save(product);
        // Only the specific productId entry is evicted
    }
}

Write-Through with @CachePut

@CachePut always executes the method and updates the cache with the result. Use this when you want to cache the result of an update operation so subsequent reads get the fresh data without a separate eviction call.

import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    @CachePut(cacheNames = "products", key = "#result.id")
    public Product updateProduct(Long productId, Product product) {
        Product saved = productRepository.save(product);
        return saved;  // Cache is updated with the returned product
    }
}

Class-Level Configuration with @CacheConfig

When multiple methods in a class share the same cache configuration, @CacheConfig keeps things DRY.

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;

@Service
@CacheConfig(cacheNames = "products")
public class ProductService {

    @Cacheable
    public Product getProduct(Long productId) {
        return productRepository.findById(productId).orElseThrow();
    }

    @CacheEvict(key = "#productId")
    public void deleteProduct(Long productId) {
        productRepository.deleteById(productId);
    }
}

Redis Configuration

Spring Boot auto-configures a RedisCacheManager when Redis is on the classpath and spring.cache.redis.* properties are set. The YAML below sets per-cache TTL values.

spring:
  cache:
    type: redis
    cache-names: products, categories, users
    redis:
      time-to-live: 10m
      cache-null-values: false
  data:
    redis:
      host: localhost
      port: 6379
      password:

For more control over serialization strategy, define a RedisCacheConfiguration bean.

import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;

@Configuration
public class RedisCacheConfig {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())
            )
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())
            )
            .disableCachingNullValues();

        return org.springframework.data.redis.cache.RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(java.util.Map.of(
                "products", config.entryTtl(Duration.ofMinutes(30)),
                "users", config.entryTtl(Duration.ofMinutes(5))
            ))
            .build();
    }
}

Cache Key Generation

Spring uses a KeyGenerator to create cache keys from method parameters. The default strategy concatenates the bean class name, method name, and parameter values. For cleaner keys, use SpEL expressions in the key attribute.

// Default key generation: productService.getProduct(42) -> "productService.getProduct(42)"

// Custom key with SpEL
@Cacheable(cacheNames = "products", key = "#productId")
public Product getProduct(Long productId) { ... }

// Composite key
@Cacheable(cacheNames = "products", key = "#category + ':' + #page")
public List<Product> getProductsByCategory(String category, int page) { ... }

// Custom KeyGenerator bean
@Bean
public KeyGenerator productKeyGenerator() {
    return (target, method, params) -> {
        StringBuilder builder = new StringBuilder();
        builder.append(target.getClass().getSimpleName());
        builder.append(".").append(method.getName());
        for (Object param : params) {
            builder.append(":").append(param.toString());
        }
        return builder.toString();
    };
}

Observability Checklist

Cache behavior directly impacts application reliability. Build these observability practices into your caching implementation from day one.

  • Log cache hit/miss rates at the application level. Most cache providers expose metrics through JMX or Micrometer. Track cache hit ratio as a percentage, and alert when it drops below a threshold that makes caching economically sensible.
  • Monitor cache size and eviction frequency. A cache that grows without bound indicates a TTL or size limit problem. High eviction rates relative to cache size suggest your cache is too small for the working set.
  • Track latency percentiles with and without cache. Cache hits should be sub-millisecond. If cache hits are slow, investigate serialization overhead or network latency to the cache store.
  • Alert on error rates from the cache layer. Connection timeouts, serialization failures, and rejected writes all surface as exceptions. Route these to your alerting system.
  • Log eviction events for significant keys. When a cache entry is evicted due to expiration or explicit invalidation, log the key name and eviction reason. This makes post-mortem analysis tractable.
  • Correlate cache metrics with business metrics. If your checkout conversion rate drops and your cache miss rate spikes simultaneously, you have a causality link that points directly at the cache layer.
  • Instrument cache key generation failures. Key generation that throws exceptions will silently bypass the cache and flood your backend. Monitor for these error patterns.

Security Notes

Sensitive Data in Cache

Caching sensitive data requires the same access controls you apply to the database. If you cache user personal information, payment details, or credentials, that data now lives in an additional store that must be encrypted at rest, encrypted in transit, and subject to access auditing. In multi-tenant environments, verify that cache isolation is sufficient to prevent tenant A from accessing tenant B’s cached data through key collision or shared infrastructure. The practical rule: never cache sensitive data unless you have a specific performance requirement that demands it and a security architecture that actually protects it.

Cache Poisoning

Cache poisoning happens when someone manipulates the data that gets stored in your cache. If your application fetches data from an external API or user-supplied source and caches it without validation, a malicious actor who controls that source can inject invalid, malicious, or inappropriate content. Subsequent users who request that data receive the poisoned content. Defenses include validating all data before caching, using input sanitization at cache write time, and applying content security policies that limit what cached data can do when rendered.

Common Pitfalls / Anti-Patterns

Serialization Issues

Redis and Memcached require objects to be serialized to bytes for storage. The default Java serialization is convenient but fragile: if you change a class structure (add fields, rename fields, change types), cached objects from previous versions will fail to deserialize. Use a stable serialization format like JSON (through GenericJackson2JsonRedisSerializer or Jackson2JsonRedisSerializer) or Protocol Buffers. JSON is human-readable during debugging, which is a practical advantage over binary formats. If you are working with API responses that need consistent serialization, the API contracts post covers related versioning strategies.

Key Collisions

If two different methods in different services share the same cache name and key format, they can overwrite each other’s entries. A product.getProduct(1) in the product service and order.getProduct(1) in the order service would collide if both use cacheNames = “products” and a key derived only from the ID. Scope your cache names to the service: cacheNames = “productServiceProducts” or use namespaced keys.

Cache Stampede

Also called the thundering herd problem. When a popular cache entry expires or a cache node fails, requests simultaneously discover the cache miss and all hit the backend database at once. The solution is request coalescing: when multiple threads request the same key simultaneously, only one thread fetches from the database while the others wait. Spring Boot does not provide this out of the box, but Caffeine or Redis-based solutions like Redisson provide cache stampede protection through distributed locks or synchronized loading patterns.

Production Failure Scenarios

Cache failures do not always look like cache failures. The symptoms often surface as database timeouts or mysterious data inconsistencies.

Stale Data Causing Business Logic Errors

Caching data that changes frequently produces stale reads. A user updates their profile, the write succeeds, but subsequent reads return the old data until the TTL expires. In reporting or billing contexts, this can produce incorrect results that pass validation because the data looks valid in isolation. The fix is active invalidation on every write path that touches the underlying data. TTL values act as a safety net, not a correctness mechanism. If writes are frequent and scattered across multiple services, caching may not be the right tool.

Cache Penetration Exhausting Database Under Load

An attacker or a bug sends requests for keys that do not exist, over and over. Each request bypasses the cache because the key is absent, hits the database, and returns nothing. At scale, this hammers the database with queries for keys that will never match. The standard fix is caching negative results: when a query returns null, store a sentinel value with a short TTL rather than nothing. Repeated requests for the same absent key return from the cache without touching the database.

Cache Stampede Causing Thundering Herd

When a high-traffic cache entry expires simultaneously or a cache node fails, all requests that would have hit that entry miss at once. Without cache stampede protection, each miss fires a backend query simultaneously, overwhelming the database with duplicate requests for the same data. Request coalescing solves this: when multiple threads request the same key at the same time, only one thread fetches from the backend and the others wait for that result. Caffeine’s LoadingCache and Redis-based solutions with lock-based loading all provide this protection.

Serialization Failure After Class Structure Changes

Changing a Java class that has cached instances in Redis or Memcached causes deserialization to fail. A field is renamed, a type is changed, or a class is removed. The old serialized bytes can no longer be parsed by the new class definition. The result is a cache full of unusable entries and a spike in cache misses that hit the database. Avoid Java default serialization for cached objects. Use stable formats like JSON through GenericJackson2JsonRedisSerializer or Protocol Buffers. Include version information in cached objects so migration strategies can detect and handle older formats.

Quick Recap Checklist

  • Add @EnableCaching to your configuration class before using any caching annotations.
  • Use @Cacheable for read operations where the same inputs repeatedly produce the same outputs.
  • Use @CacheEvict on every write path that modifies data backing a cache.
  • Use @CachePut when you want to update cached data on writes without a separate eviction call.
  • Set reasonable TTL values as a safety net even when you use active invalidation.
  • Choose in-memory caching for single-instance applications, Redis for distributed systems.
  • Configure serialization explicitly. Do not rely on default Java serialization for production data.
  • Monitor cache hit rate, latency percentiles, and eviction counts.
  • Never cache sensitive data without evaluating the security implications.
  • Implement cache stampede protection for high-traffic, high-value cache entries.
  • Use scoped cache names to prevent key collisions across services.

Interview Questions

1. What is the difference between @Cacheable and @CachePut in Spring Boot?

@Cacheable guards method execution with a cache lookup. If a cached value exists for the given key, the method body is never executed. If no value exists, the method runs and its result is stored in the cache. This is the standard read-through pattern.

@CachePut always executes the method body regardless of whether the key exists in the cache, and then updates the cache with the returned value. Use it when you want to write data and refresh the cache in a single operation, such as after an update or save operation where subsequent reads should get the fresh data without a separate eviction call.

2. How do you handle cache invalidation when data changes in multiple places?

Place @CacheEvict annotations on every write operation that modifies the underlying data. This creates a contract: every method that changes data is responsible for removing stale entries from the cache.

For distributed systems where a write might happen in one service and a read in another, consider a shared distributed cache like Redis so eviction in one service is immediately visible to readers in another. You can also implement event-driven invalidation where write operations publish domain events that subscribers consume to evict relevant cache entries. Event-driven approaches pair well with the [microservices patterns](/blog/roadmaps/microservices/) covered in the learning path. The key principle is that cache invalidation must be explicit and cover every write path. Missing a single write path results in stale data that persists until TTL expiration.

3. What is cache penetration and how do you prevent it?

Cache penetration occurs when requests repeatedly target cache keys that do not exist. Each request bypasses the cache because the key is not present, hits the database, and returns nothing. An attacker exploiting this can send a flood of requests for non-existent keys to overwhelm your database.

Cache negative results. When a database query returns null, store a sentinel value (or the null result itself with cache-null-values: false in Redis) in the cache with a short TTL. This blocks repeated queries for the same non-existent key from hitting the database. Additionally, validate input parameters and reject obviously invalid keys before they reach the cache layer.

4. Explain cache avalanche and how to prevent it.

A cache avalanche happens when a large number of cached entries expire at approximately the same time, causing a sudden spike in backend load as all those requests miss the cache simultaneously. This commonly occurs when you set identical TTL values on many entries during a deployment, or when a cache node restarts and its contents are lost.

Prevention strategies: add jitter to TTL values so entries expire at different times rather than in lockstep, pre-warm the cache after restarts by proactively loading critical keys, and use request coalescing so that when a cache miss occurs, only one thread fetches the backend value while others wait for that result rather than all hammering the database simultaneously.

5. What are the key differences between in-memory caching (Caffeine) and Redis for Spring Boot applications?

Caffeine operates entirely in-process within the JVM. No network latency, no serialization overhead, no operational complexity. It works well for single-instance applications where cache sharing across nodes is not required and when the working set fits comfortably in heap memory. The tradeoff is that cached data is lost when the application restarts, and you cannot share the cache across multiple application instances.

Redis is an external distributed cache that runs as a separate process. It adds network round-trip latency (typically 1-5ms per operation) and requires you to serialize objects to bytes. In return, you get shared state across all application instances, persistence options, rich data structures, and cluster scaling. For microservices architectures where multiple instances need a consistent view of cached data, Redis is the practical choice.

If you run one instance, use Caffeine. If you run multiple instances or need cache persistence, use Redis.

6. How does @CacheEvict work and when would you use allEntries = true?

@CacheEvict removes entries from the cache when a method executes. The key attribute targets a specific cache entry for removal, while allEntries = true clears the entire named cache in one operation.

Use allEntries = true when you need a complete cache refresh, such as during a data import, a configuration reload, or when underlying reference data has been fully replaced rather than incrementally updated. The tradeoff is that you evict useful entries that may still be valid, forcing a cache warm-up period after the clear. Only use it when the operation genuinely invalidates the entire dataset rather than a subset.

7. What is the purpose of the key attribute in @Cacheable and how does SpEL work there?

The key attribute defines how cache keys are generated from method parameters. Spring uses SpEL (Spring Expression Language) to evaluate the key expression against the method's arguments.

Common patterns: #productId references a parameter directly, #result.id accesses the return value after method execution, #p0 uses positional parameter notation, and composite keys like #category + ':' + #page combine multiple values. The default key generation, when no key is specified, uses all method parameters, which can lead to unwieldy keys for methods with many arguments.

A custom KeyGenerator bean lets you define consistent key generation strategies across multiple methods without repeating SpEL expressions everywhere.

8. What is cache stampede and what solutions exist in the Spring ecosystem?

Cache stampede (thundering herd) occurs when a popular cache entry expires or a cache node fails, causing all concurrent requests for that key to miss simultaneously and hit the backend all at once. This can overwhelm your database or downstream service.

Solutions in the Spring ecosystem: Caffeine's LoadingCache provides get(key, loader) which synchronizes concurrent loads for the same key automatically. Redis-based solutions like Redisson's RedissonLock can coordinate distributed locking so only one thread fetches the backend while others wait. Request coalescing libraries like Spring's RequestContextHolder can deduplicate in-flight requests at the application level.

9. How does cache-null-values: false affect caching behavior?

By default, Spring Cache caches null values to prevent repeated database queries for non-existent data (cache penetration protection). Setting cache-null-values: false disables this behavior.

When cache-null-values: false, a null return from the method means nothing gets stored in the cache for that key. Each subsequent request for the same key hits the backend again. This is appropriate when null results are expected to be rare and caching them would waste cache space, or when you want to distinguish between "we looked and found nothing" versus "we have not looked yet."

10. What is the difference between @CacheConfig at the class level and specifying cache names on each method?

@CacheConfig is a class-level annotation that centralizes common cache settings for all methods in the class. When you specify cacheNames on @CacheConfig, individual method annotations can omit the cache name because they inherit it from the class-level configuration.

This reduces repetition and makes it easier to change cache names across multiple methods. However, method-level attributes always override class-level ones. So a method can still specify its own key, cacheNames, or other attributes to deviate from the class defaults when needed.

11. When should you avoid using Spring's annotation-based caching entirely?

Avoid annotation-based caching when cache logic is dynamic, conditional, or involves complex data-dependent decisions. If the decision to cache depends on runtime context (user permissions, feature flags, request headers), annotations become limiting.

Also avoid them when you need atomic cache operations across multiple keys, or when cache interactions must be part of a broader transaction that spans non-cache operations. In these cases, inject CacheManager directly and write explicit cache logic in your service code for full control over the cache lifecycle.

12. How does Redis cache serialization work with Jackson and what are the pitfalls?

When using Redis with Spring Boot, GenericJackson2JsonRedisSerializer serializes objects to JSON strings stored as Redis bytes. On deserialization, it includes type metadata in the JSON ($type field) so Jackson can reconstruct the correct class.

Pitfalls: if you change the class structure (rename a field, change a type, refactor a package), old cached entries fail to deserialize because the $type metadata points to the old class definition. Solutions include using versioned serialization formats, implementing custom Jackson mixins for backward compatibility, or running cache migration scripts during deployments that rewrite old entries.

13. What metrics should you monitor for a production Redis cache in a Spring Boot application?

Key metrics: cache hit ratio (hits divided by total gets), cache miss rate, latency percentiles for cache operations (P50, P99), eviction count and rate, memory usage per cache, and connection pool utilization.

At the application level, monitor cache.gets, cache.puts, cache.evictions via Micrometer. At the Redis level, track used_memory, connected_clients, rejected_connections. Alert on: hit ratio dropping below threshold, latency spiking above normal, eviction rate increasing, and connection pool exhaustion.

14. How can you implement conditional caching based on method return value in Spring?

Use the unless attribute in @Cacheable with a SpEL expression that evaluates against the method result. For example, unless = "#result == null" skips caching when the method returns null, or unless = "#result.size() > 100" avoids caching very large collections.

Other conditional attributes: condition evaluates against method arguments before the method executes, while unless evaluates against the return value after execution. Combine both for fine-grained control over what gets cached under which circumstances.

15. What is the difference between Redis Cache and Spring Cache abstraction in Spring Boot?

Spring Cache abstraction is a layer that sits on top of various cache providers. It provides the annotation model (@Cacheable, @CacheEvict, etc.) and a CacheManager interface that adapters implement for different backends.

Redis Cache, specifically, is a cache type that Spring Boot auto-configures when Redis is on the classpath. It uses RedisCacheManager to store cache entries in Redis. You can also use Spring Cache with Caffeine, ConcurrentMap, or other backends without changing your annotation-based code. The annotations are provider-agnostic; the CacheManager handles the storage details.

16. How does TTL work with Spring Boot Redis caching and can different caches have different TTLs?

TTL (time-to-live) controls how long entries remain in the cache before automatic expiration. In Spring Boot Redis caching, set spring.cache.redis.time-to-live for a global default, or configure per-cache TTLs using RedisCacheConfiguration with withInitialCacheConfigurations.

Each cache can have its own TTL. For example, you might set a 30-minute TTL for a product catalog cache that changes infrequently and a 5-minute TTL for a user session cache that updates more often. Entries that exceed their TTL are evicted on next access or by Redis's background expiration mechanism.

17. What strategies exist for warming up the cache after application startup?

Cache warm-up strategies: use @PostConstruct or ApplicationReadyEvent to trigger methods annotated with @Cacheable for known popular keys, pre-populate the cache using CacheManager.getCache(cacheName).put(key, value) during startup, or implement a scheduled job that runs on startup to load critical data.

For Redis, you can also pre-load data by querying the database and explicitly putting entries into the cache before the application begins handling traffic. The key is identifying which data is popular enough to warrant pre-loading versus which can be loaded lazily on first request.

18. What is the role of CacheManager in Spring Boot caching and how do you customize it?

CacheManager is the core interface that Spring Cache abstraction uses to interact with underlying cache implementations. It provides getCache(name) to retrieve a cache by name and manages the lifecycle of cache instances.

Customize by defining a CacheManager bean in your configuration class. For Redis, this lets you control serialization strategy, TTL defaults, per-cache configurations, and cache-level settings like disableCachingNullValues. Spring Boot auto-configuration backs off when a CacheManager bean is present, giving you full control.

19. How does caching interact with Spring Security and what pitfalls exist?

If your cached data is user-specific, cache keys must incorporate user identity (user ID, tenant ID) to prevent cross-user data leakage. A user requesting their own profile could receive another user's cached profile if the cache key only uses the profile ID without user context.

Another pitfall: caching data that includes authorization decisions. If a user's permissions change, cached data reflecting old permissions remains until TTL expiration. Always separate raw data caching from authorization state, and consider whether caching user-specific data is worth the consistency tradeoff.

Further Reading

Conclusion

Spring Boot’s caching annotations provide a powerful and flexible abstraction for reducing database load and improving response times, but that simplicity requires discipline to avoid subtle production bugs. The annotation model handles the happy path well, but cache invalidation, TTL management, and serialization strategy demand careful attention.

The most important principles to carry forward: cache invalidation must be explicit and cover every write path, TTL values serve as a safety net rather than a correctness mechanism, and serialization choices must anticipate class evolution. For single-instance applications, in-memory caching with Caffeine provides the simplest path to performance gains. For distributed systems, Redis delivers shared state and persistence at the cost of network round-trips and serialization complexity.

Monitoring cache behavior in production is non-negotiable. Track hit rates, eviction counts, and latency distributions for cache operations. Alert on deviations that suggest stale data serving or cache exhaustion. When done well, caching is invisible to callers and delivers consistent performance gains. When done poorly, it surfaces as mysterious data inconsistencies and production incidents that are difficult to diagnose.

Category

Related Posts

Spring Boot Build Tools: Maven & Gradle

Configure Maven and Gradle for Spring Boot projects—plugins, dependency management, packaging JARs and WARs, and build automation essentials.

#spring-boot #spring-boot-roadmap #learning-path

Embedded Web Servers in Spring Boot: Tomcat, Jetty, Undertow

Configure embedded servers in Spring Boot: compare Tomcat, Jetty, and Undertow, tune thread pools, enable access logs, and switch implementations.

#spring-boot #spring-boot-roadmap #learning-path

JUnit 5 & Jupiter: Lifecycle, Nested & Parameterized Tests

Explore JUnit 5 Jupiter features: master test lifecycle annotations, organize tests with @Nested, and parameterize tests with @CsvSource and @MethodSource.

#spring-boot #spring-boot-roadmap #learning-path