Posted on: July 2, 2025 Posted by: rahulgite Comments: 0

Caching is a performance optimization technique that stores frequently accessed data in memory to avoid repetitive and expensive computations or database calls.


1. Why Use Cache?

  • Reduce database access latency
  • Improve throughput for read-heavy applications
  • Lower load on the backend database

2. Types of Caches

1. First-Level Cache (Persistence Context Cache)

  • Scope: Per EntityManager
  • Enabled by default in JPA
  • Behavior: Caches entities within a transaction or session
  • Usage: Transparent to developers

2. Second-Level Cache

  • Scope: Shared across sessions/transactions
  • Enabled manually with caching providers like:
    • EhCache
    • Caffeine
    • Infinispan
  • Usage: Configure with JPA and provider-specific XML or Java config
  • Annotations: Works with @Cacheable, @CacheEvict

3. Application-Level Cache (Spring Cache Abstraction)

Spring Cache abstracts the caching mechanism and lets you plug in your own provider:

  • Simple Cache (Default): Uses ConcurrentMapCacheManager – basic, thread-safe in-memory cache
  • EhCache: XML-configurable, mature, widely used
  • Caffeine: High-performance Java caching library
  • Redis (external): Suitable for distributed systems and large-scale caching

2.1 Steps to Add Each Cache Provider

✅ Overview of All Cache Managers in Spring Boot

Spring Boot auto-configures these commonly used cache managers:

Cache ManagerDescription
ConcurrentMapCacheManagerDefault, simple in-memory, thread-safe
EhCacheCacheManagerWraps EhCache (requires ehcache.xml or config)
JCacheCacheManagerJSR-107 standard, used with EhCache 3, Hazelcast
CaffeineCacheManagerHigh-performance local cache
RedisCacheManagerRedis-based distributed cache
SimpleCacheManagerProgrammatic setup with custom Cache beans
NoOpCacheManagerDisables caching, useful for testing

✅ How to Choose Cache Manager

Spring Boot detects the cache manager based on the available dependencies:

  • No dependency: ConcurrentMapCacheManager is used
  • Ehcache: EhCacheCacheManager
  • Caffeine: CaffeineCacheManager
  • JCache (JSR-107): JCacheCacheManager
  • Redis: RedisCacheManager

You can override the default by providing a @Bean of type CacheManager in a @Configuration class.

✅ Example to Select a Specific Cache Manager

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new CaffeineCacheManager("users", "products");
    }
}

Each manager can have its own behavior regarding TTL, eviction policy, serialization, etc., and should be chosen based on the application scale and requirements.

✅ Default (Simple In-Memory Cache)

  • No dependency required
  • Configuration:
@Bean
public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("users");
}

✅ EhCache (XML-based)

Dependency:

<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
</dependency>

Steps:

  1. Create ehcache.xml under resources
  2. Define cache name, max entries, TTL, etc.
  3. Configure EhCacheManager:
@Bean
public JCacheManagerCustomizer customizer() {
    return cm -> cm.createCache("users", new MutableConfiguration<>());
}

✅ Caffeine (Recommended for high performance)

Dependency:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Configuration:

@Bean
public CacheManager caffeineCacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager("users");
    manager.setCaffeine(Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(100));
    return manager;
}

✅ Redis (for distributed caching)

Dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

Configuration:

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(10));
    return RedisCacheManager.builder(connectionFactory)
        .cacheDefaults(config)
        .build();
}

When to Use What:

Cache TypeBest For
SimpleSmall apps, testing, no external deps
EhCacheXML-configurable legacy enterprise apps
CaffeineHigh-performance JVM-based apps
RedisDistributed apps or multi-node architecture

1. First-Level Cache (Persistence Context Cache)

  • Scope: Per EntityManager
  • Enabled by default in JPA
  • Behavior: Caches entities within a transaction or session
  • Usage: Transparent to developers

2. Second-Level Cache

  • Scope: Shared across sessions/transactions
  • Configured manually using a provider like:
    • EhCache
    • Caffeine
    • Infinispan
  • Needs annotations: @Cacheable, @CacheEvict

3. Application-Level Cache (Spring Cache Abstraction)

  • Provided by Spring Framework
  • Use any caching provider (EhCache, Redis, etc.)

3. Spring Cache Annotations

@Cacheable

Caches method results.

@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {...}

Attributes:

  • value: cache name
  • key: SpEL expression for custom key
  • condition: cache only if true
  • unless: skip caching if true

@CachePut

Always executes the method and updates the cache with the result.

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {...}

@CacheEvict

Removes data from the cache.

@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {...}

Attributes:

  • key: evict specific key
  • allEntries = true: remove all entries
  • beforeInvocation = true: evict before method runs

4. Example with Configuration

Setting Expiry for Each Cache

Expiration (TTL – Time To Live) defines how long an entry stays in the cache. It is supported differently by cache providers.

✅ Caffeine (per cache or global)

@Bean
public CacheManager caffeineCacheManager() {
    CaffeineCacheManager manager = new CaffeineCacheManager("users", "products");
    manager.setCaffeine(Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .maximumSize(100));
    return manager;
}

✅ Redis

@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
    RedisCacheConfiguration usersCache = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(5));

    RedisCacheConfiguration productsCache = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofHours(1));

    Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
    cacheConfigs.put("users", usersCache);
    cacheConfigs.put("products", productsCache);

    return RedisCacheManager.builder(connectionFactory)
        .withInitialCacheConfigurations(cacheConfigs)
        .build();
}

✅ EhCache

Configure TTL in ehcache.xml:

<cache name="users" maxEntriesLocalHeap="1000" timeToLiveSeconds="600" />
<cache name="products" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600" />

✅ Simple Cache (ConcurrentMapCacheManager)

Does not support TTL by default. Use alternative like Caffeine or Redis for expiry-based caching.


Example 1: Simple Cache with Custom Key and Condition

@Cacheable(value = "products", key = "#productId", condition = "#productId > 10", unless = "#result == null")
public Product getProductById(Long productId) {
    return productRepository.findById(productId).orElse(null);
}

Example 2: Cache with All Entries and Custom Parameter Logic

@CacheEvict(value = "products", allEntries = true, beforeInvocation = false)
public void clearProductCache() {
    // This method will clear all entries from 'products' cache
}

Example 3: Cache Based on Complex Object Parameter

@Cacheable(value = "users", key = "#user.email + ':' + #user.country")
public User getUserDetails(User user) {
    return userService.findByEmailAndCountry(user.getEmail(), user.getCountry());
}

Example 1: Simple Cache with Custom Key and Condition

@Cacheable(value = "products", key = "#productId", condition = "#productId > 10", unless = "#result == null")
public Product getProductById(Long productId) {
    return productRepository.findById(productId).orElse(null);
}
  • value: Defines the cache name
  • key: Uses the method parameter productId as the cache key
  • condition: Caches only if productId is greater than 10
  • unless: Skips caching if the result is null

Example 2: Cache with All Entries and Custom Parameter Logic

@CacheEvict(value = "products", allEntries = true, beforeInvocation = false)
public void clearProductCache() {
    // This method will clear all entries from 'products' cache
}
  • allEntries = true: Clears the entire cache region
  • beforeInvocation = false: Eviction happens after method execution

Example 3: Cache Based on Complex Object Parameter

@Cacheable(value = "users", key = "#user.email + ':' + #user.country")
public User getUserDetails(User user) {
    return userService.findByEmailAndCountry(user.getEmail(), user.getCountry());
}
  • key: Composite key generated from multiple fields of the input object
@EnableCaching
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users", "products");
    }
}

5. Lazy vs Eager Loading

Lazy Loading

  • Loads associations on-demand
  • Default strategy
  • Requires open session when accessing
@ManyToOne(fetch = FetchType.LAZY)
private Department department;

Eager Loading

  • Loads associations immediately
  • Can lead to unnecessary joins
@ManyToOne(fetch = FetchType.EAGER)
private Department department;

When to Use:

  • Lazy: Most cases; better performance
  • Eager: When you always need associated data immediately

6. JPA Proxies

JPA uses proxy classes to support lazy loading. These are subclasses generated at runtime.

Example:

Employee emp = entityManager.find(Employee.class, 1L);
Department dept = emp.getDepartment(); // department is a proxy

Note: Accessing a proxy outside of the transaction/session will throw LazyInitializationException.


7. Best Practices for Caching

  • Use @Cacheable on id-based read methods
  • Evict or update cache on writes with @CacheEvict or @CachePut
  • Use meaningful key values to avoid collisions
  • Don’t cache frequently changing data
  • Always monitor memory usage and cache hit rates
  • Use TTL (time to live) settings in external caches

8. Visual Diagram of Cache Flow

              +-------------------+
              |  Spring Service   |
              +---------+---------+
                        |
                        v
               +--------+--------+
               |  Spring Cache   | <-- First-level (if inside JPA Tx)
               +--------+--------+
                        |
                        v
               +--------+--------+
               |  EntityManager  |
               +--------+--------+
                        |
                        v
                    Database

Leave a Comment