Spring Boot’s caching abstraction is surprisingly flexible, but its default behavior often hides the fact that you’re not actually using Redis or EhCache until you explicitly tell it to.

Let’s see what happens when we try to cache some data. Imagine a simple service that fetches user information:

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

@Service
public class UserService {

    @Cacheable("users")
    public User getUserById(Long id) {
        System.out.println("Fetching user from database for ID: " + id);
        // Simulate database call
        return new User(id, "User " + id);
    }
}

And a User class:

public class User {
    private Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() { return id; }
    public String getName() { return name; }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", name='" + name + '\'' +
               '}';
    }
}

Without any specific cache configuration, if you run this and call userService.getUserById(1L) twice, you’ll see "Fetching user from database for ID: 1" printed twice. Spring Boot has a default in-memory cache implementation (usually ConcurrentMapCacheManager) that’s activated if no other cache provider is configured. This means your @Cacheable annotation is doing something, but it’s not reaching out to Redis or EhCache yet.

To make Spring Boot use Redis, you need a few things. First, add the spring-boot-starter-data-redis dependency. Then, you need to configure a RedisCacheManager.

In your application.properties (or application.yml), you’ll want to set up your Redis connection:

spring.redis.host=localhost
spring.redis.port=6379

And then, in your configuration class, you’ll define the RedisCacheManager:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;

import java.time.Duration;

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        return builder -> builder
            .withCacheConfiguration("users",
                RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)));
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        return RedisCacheManager.builder(connectionFactory)
            .configureCache("users", RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10))) // Explicitly configure 'users' cache
            .build();
    }
}

With this setup, when userService.getUserById(1L) is called the first time, the "Fetching user…" message appears, and the result is stored in Redis under the key users::1. The second call will fetch it directly from Redis, and you won’t see the "Fetching user…" message. The entryTtl(Duration.ofMinutes(10)) sets a Time-To-Live for cache entries in Redis, ensuring they expire after 10 minutes.

For EhCache, the process is similar but uses a different dependency and configuration. Add spring-boot-starter-cache and ehcache (or spring-data-ehcache if you need more advanced features).

You’ll need an ehcache.xml configuration file in your src/main/resources directory:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">

    <cache alias="users">
        <expiry>
            <time unit="minutes">10</time>
        </expiry>
        <resources>
            <heap unit="entries">1000</heap>
        </resources>
    </cache>
</config>

And in your configuration class, you’ll enable caching and let Spring Boot auto-configure EhCache:

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

@Configuration
@EnableCaching
public class CacheConfig {
    // No explicit CacheManager bean needed here if EhCache is the only provider
    // and you have ehcache.xml in resources. Spring Boot's auto-configuration will pick it up.
}

When you run the UserService with this EhCache configuration, the first call to getUserById(1L) will print the "Fetching user…" message. The result is then stored in EhCache. The second call will hit the EhCache, and the message won’t be printed. The expiry and heap configurations in ehcache.xml control how long items stay in the cache and how many can be held in memory, respectively.

The most surprising thing about Spring’s caching abstraction is that @Cacheable doesn’t inherently know about Redis or EhCache. It’s a generic annotation that delegates to a CacheManager. Spring Boot’s auto-configuration is what wires up the specific CacheManager (like RedisCacheManager or JCacheManager for EhCache) based on your dependencies and configuration. If you don’t provide a specific CacheManager bean or auto-configuration trigger (like spring-boot-starter-data-redis with a Redis connection), you get the default in-memory ConcurrentMapCacheManager.

The exact levers you control are primarily through the CacheManager configuration. For Redis, this means setting up connection details and defining RedisCacheConfiguration (including TTLs, serialization, etc.) for specific cache names. For EhCache, it involves providing an ehcache.xml with cache definitions or using programmatic configuration to specify expiry, resource limits (heap, off-heap), and eviction policies. The @Cacheable annotation itself is simple, but the underlying CacheManager is where the real power and complexity lie.

A common pitfall is forgetting to set a TTL for Redis caches; without it, entries will live forever until manually removed or Redis restarts, which is rarely the desired behavior.

The next step in mastering Spring Boot caching often involves understanding cache invalidation strategies, particularly with @CacheEvict and custom key generation.

Want structured learning?

Take the full Spring-boot course →