Spring Boot Caching

Caching is a key strategy in software development to enhance application performance and optimize resource utilization. In the context of Java applications, Spring Boot provides an elegant and straightforward way to implement caching using its built-in framework support. This article explores the essentials of Spring Boot caching, from its core concepts to practical implementations, helping developers leverage its power to build high-performance applications.

What is Caching?

Caching stores frequently accessed data in memory so that subsequent requests for the same data can be served faster. Caching can significantly improve application speed and responsiveness by reducing the time spent on expensive operations like database queries, computations, or API calls.

Key benefits of caching include:

  • Improved Performance: Minimizes latency by retrieving data from a faster storage layer.
  • Reduced Load: Decreases the load on external resources like databases or APIs.
  • Enhanced Scalability: Handles a higher volume of requests efficiently.

Spring Boot Caching Overview

Spring Boot builds on the Spring Framework’s abstraction for caching, offering a declarative programming model. It allows developers to integrate caching effortlessly using annotations. Spring Boot supports multiple caching providers like EhCache, Redis, Hazelcast, and Caffeine, making it flexible for diverse use cases.

Core Components of Spring Boot Caching

  1. Cache Abstraction: Spring’s caching abstraction provides a consistent programming model across different implementations.
  2. Annotations:
  • @EnableCaching: Activates Spring’s annotation-driven cache management.
  • @Cacheable: Indicates that a method’s result should be cached.
  • @CachePut: Updates the cache without affecting the method execution.
  • @CacheEvict: Removes entries from the cache.
  • @Caching: Allows combining multiple cache operations.
  1. Cache Manager: The CacheManager interface defines the contract for cache management and works with underlying cache providers.

Setting Up Spring Boot Caching

To get started with caching in Spring Boot, follow these steps:

Step 1: Add Dependencies

Include the necessary dependencies in your pom.xml or build.gradle file based on your caching provider. For example, for EhCache:

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

Step 2: Enable Caching

Enable caching in your Spring Boot application by adding the @EnableCaching annotation to a configuration class:

@Configuration
@EnableCaching
public class CacheConfig {
    // Additional cache configuration (optional)
}

Step 3: Configure Cache Manager

Spring Boot provides default configurations for popular cache providers. For custom setups, define a bean for CacheManager:

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

Using Spring Boot Caching Annotations

1. @Cacheable

The @Cacheable annotation caches the result of a method based on its parameters. If the same parameters are passed again, the cached result is returned.

@Cacheable("users")
public User getUserById(Long id) {
    // Expensive database operation
    return userRepository.findById(id).orElseThrow(() -> new UserNotFoundException());
}

2. @CachePut

The @CachePut annotation updates the cache with the method’s result every time it is invoked.

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

3. @CacheEvict

The @CacheEvict annotation removes entries from the cache for a specific key or all entries.

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

To clear all entries in a cache, use:

@CacheEvict(value = "users", allEntries = true)
public void clearUsersCache() {
    // Clear operation
}

4. @Caching

For complex scenarios, use @Caching to combine multiple caching operations:

@Caching(
    put = { @CachePut(value = "users", key = "#user.id") },
    evict = { @CacheEvict(value = "users", key = "#user.id") }
)
public User saveOrUpdateUser(User user) {
    return userRepository.save(user);
}

5. Create RESTful

Create a rest controller.

@RestController
@RequestMapping("/api/v1/users-cache")
@Slf4j
@RequiredArgsConstructor
public class UserCacheController {

    private final UserProfileCacheService userProfileCacheService;

    private final  CacheManager cacheManager;

    @GetMapping("/{id}")
    public ResponseEntity<UserProfile> getUserById(@PathVariable String id) {
        UUID uuid = UUID.fromString(id);
        UserProfile user = userProfileCacheService.getUserById(uuid);
        printAllCachesAndEntries();
        return ResponseEntity.ok(user);
    }

    @PostMapping("/{name}")
    public ResponseEntity<UserProfile> createUserProfile(@PathVariable String name) {
        // Create a demo UserProfile instance using @Builder
        UserProfile demoUserProfile = new UserProfile();
        demoUserProfile.setId(UUID.randomUUID());
        demoUserProfile.setFirstName(name);
        demoUserProfile.setLastName("Doe");
        demoUserProfile.setEmail("john.doe@example.com");
        demoUserProfile.setBirthDate(LocalDateTime.of(1990, 5, 15, 0, 0));
        demoUserProfile.setSex(1);
        demoUserProfile.setCreateBy(UUID.randomUUID());
        demoUserProfile.setCreateDate(LocalDateTime.now());
        demoUserProfile.setUpdateBy(null);
        demoUserProfile.setUpdateDate(null);
        UserProfile user = userProfileCacheService.updateUser(demoUserProfile);
        printAllCachesAndEntries();
        return ResponseEntity.ok(user);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<String> deleteUserProfile(@PathVariable String id) {
        UUID uuid = UUID.fromString(id);
        userProfileCacheService.deleteUser(uuid);
        UUID key = findCacheKeyByUserProfileId(uuid);
        userProfileCacheService.deleteCacheByKey(key);
        printAllCachesAndEntries();
        return ResponseEntity.ok("delete success.");
    }

    @DeleteMapping
    public ResponseEntity<String> clearCache() {
        userProfileCacheService.clearUsersCache();
        printAllCachesAndEntries();
        return ResponseEntity.ok("clear cache success.");
    }

    public void printAllCachesAndEntries() {
        if (cacheManager instanceof ConcurrentMapCacheManager concurrentMapCacheManager) {
            for (String cacheName : concurrentMapCacheManager.getCacheNames()) {
                log.info("Cache Name: {}", cacheName);
                Cache cache = concurrentMapCacheManager.getCache(cacheName);
                if (cache instanceof ConcurrentMapCache concurrentMapCache) {
                    Map<Object, Object> nativeCache = concurrentMapCache.getNativeCache();
                    if (nativeCache.isEmpty()) {
                        log.info("Cache is empty.");
                    } else {
                        nativeCache.forEach((key, value) ->
                                log.info("Key: {}, Value: {}", key , value)
                        );
                    }
                } else {
                    log.info("Cache is not an instance of ConcurrentMapCache.");
                }
                log.info("--------------------------------------------------");
            }
        } else {
            log.info("CacheManager is not an instance of ConcurrentMapCacheManager.");
        }
    }

    public UUID findCacheKeyByUserProfileId(UUID userProfileId) {
        Cache cache = cacheManager.getCache("users");
        if (cache != null) {
            if (cache instanceof ConcurrentMapCache concurrentMapCache) {
                Map<Object, Object> nativeCache = concurrentMapCache.getNativeCache();
                for (Map.Entry<Object, Object> entry : nativeCache.entrySet()) {
                    UserProfile userProfile = (UserProfile) entry.getValue();
                    if (userProfile.getId().equals(userProfileId)) {
                        return (UUID)entry.getKey(); // Return the cache key (UUID)
                    }
                }
            }
        }
        return null; // Return null if no match found
    }

}

Create a cache service.

@Service
@RequiredArgsConstructor
public class UserProfileCacheService {

    private final UserProfileRepository userProfileRepository;

    @Cacheable("users")
    public UserProfile getUserById(UUID uuid) {
        return userProfileRepository.findById(uuid).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    @CachePut(value = "users", key = "#user.id")
    public UserProfile updateUser(UserProfile user) {
        return userProfileRepository.save(user);
    }

    public void deleteUser(UUID id) {
        userProfileRepository.deleteById(id);
    }

    @CacheEvict(value = "users", key = "#id")
    public void deleteCacheByKey(UUID id) {
        //
    }

    @CacheEvict(value = "users", allEntries = true)
    public void clearUsersCache() {
    }

}

Create a user profile bean.

@Data
@Entity
@Table(name = "user_profiles", schema = "public")
@NoArgsConstructor
public class UserProfile {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    @Column(name = "first_name", nullable = false)
    private String firstName;

    @Column(name = "last_name", nullable = false)
    private String lastName;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "birth_date", nullable = false)
    private LocalDateTime birthDate;

    @Column(name = "sex", nullable = false)
    private Integer sex;

    @Column(name = "create_by", nullable = false)
    private UUID createBy;

    @Column(name = "create_date", nullable = false)
    private LocalDateTime createDate;

    @Column(name = "update_by")
    private UUID updateBy;

    @Column(name = "update_date")
    private LocalDateTime updateDate;

}

Create a User profile repository.

@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, UUID> {
}

Create a cache config.

@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("users");
    }

}

6. Create a database

The developer creates a database following this tutorial.

data
table structure

Test case scenario

1. GET User by ID:
/api/v1/users-cache/81f867c4–86e9–47e6–9f50-bbf22dfe13eb

{
  "id": "81f867c4-86e9-47e6-9f50-bbf22dfe13eb",
  "firstName": "Cathleen",
  "lastName": "Mccall",
  "email": "cathleen.m@example.com",
  "birthDate": "1985-06-06T00:00:00",
  "sex": 2,
  "createBy": "4d4f65ee-cfe3-4431-88e4-b4142eccb739",
  "createDate": "2024-06-04T16:26:39.325342",
  "updateBy": null,
  "updateDate": null
}

First request.

Hibernate: select up1_0.id,up1_0.birth_date,up1_0.create_by,up1_0.create_date,up1_0.email,up1_0.first_name,up1_0.last_name,up1_0.sex,up1_0.update_by,up1_0.update_date from public.user_profiles up1_0 where up1_0.id=?
c.e.p.controller.UserCacheController     : Cache Name: users
c.e.p.controller.UserCacheController     : Key: 81f867c4-86e9-47e6-9f50-bbf22dfe13eb, Value: UserProfile(id=81f867c4-86e9-47e6-9f50-bbf22dfe13eb, firstName=Cathleen, lastName=Mccall, email=cathleen.m@example.com, birthDate=1985-06-06T00:00, sex=2, createBy=4d4f65ee-cfe3-4431-88e4-b4142eccb739, createDate=2024-06-04T16:26:39.325342, updateBy=null, updateDate=null)
c.e.p.controller.UserCacheController     : --------------------------------------------------

Second request.

c.e.p.controller.UserCacheController     : Cache Name: users
c.e.p.controller.UserCacheController     : Key: 81f867c4-86e9-47e6-9f50-bbf22dfe13eb, Value: UserProfile(id=81f867c4-86e9-47e6-9f50-bbf22dfe13eb, firstName=Cathleen, lastName=Mccall, email=cathleen.m@example.com, birthDate=1985-06-06T00:00, sex=2, createBy=4d4f65ee-cfe3-4431-88e4-b4142eccb739, createDate=2024-06-04T16:26:39.325342, updateBy=null, updateDate=null)
c.e.p.controller.UserCacheController     : --------------------------------------------------

On the second request, there is no database query. The response sends data from the cache.

2. POST Create a user by ID:/api/v1/users-cache/smith

postman request
Hibernate: insert into public.user_profiles (birth_date,create_by,create_date,email,first_name,last_name,sex,update_by,update_date,id) values (?,?,?,?,?,?,?,?,?,?)
c.e.p.controller.UserCacheController     : Cache Name: users
c.e.p.controller.UserCacheController     : Key: 6d43e9be-4e74-4828-a833-14db5ed991c3, Value: UserProfile(id=ba82bcf2-605f-4c82-8bde-8f75a04c4d1a, firstName=smith, lastName=Doe, email=john.doe@example.com, birthDate=1990-05-15T00:00, sex=1, createBy=a8ad9387-1b08-4de5-8b58-b3ddd821b531, createDate=2024-12-19T16:26:44.746871600, updateBy=null, updateDate=null)
c.e.p.controller.UserCacheController     : Key: 81f867c4-86e9-47e6-9f50-bbf22dfe13eb, Value: UserProfile(id=81f867c4-86e9-47e6-9f50-bbf22dfe13eb, firstName=Cathleen, lastName=Mccall, email=cathleen.m@example.com, birthDate=1985-06-06T00:00, sex=2, createBy=4d4f65ee-cfe3-4431-88e4-b4142eccb739, createDate=2024-06-04T16:26:39.325342, updateBy=null, updateDate=null)
c.e.p.controller.UserCacheController     : --------------------------------------------------

The user name Smith has been created and added to the cache.

data

3. DELETE User by ID:
/api/v1/users-cache/ba82bcf2-605f-4c82-8bde-8f75a04c4d1a

postman delete
Hibernate: delete from public.user_profiles where id=?
c.e.p.controller.UserCacheController     : Cache Name: users
c.e.p.controller.UserCacheController     : Key: 81f867c4-86e9-47e6-9f50-bbf22dfe13eb, Value: UserProfile(id=81f867c4-86e9-47e6-9f50-bbf22dfe13eb, firstName=Cathleen, lastName=Mccall, email=cathleen.m@example.com, birthDate=1985-06-06T00:00, sex=2, createBy=4d4f65ee-cfe3-4431-88e4-b4142eccb739, createDate=2024-06-04T16:26:39.325342, updateBy=null, updateDate=null)
c.e.p.controller.UserCacheController     : --------------------------------------------------

The user has been deleted.

4. Clear all cache: /api/v1/users-cache/

Data in cache

c.e.p.controller.UserCacheController     : Cache Name: users
c.e.p.controller.UserCacheController     : Key: a57dcfd0-9703-4852-ac89-1fb3d54310a9, Value: UserProfile(id=a57dcfd0-9703-4852-ac89-1fb3d54310a9, firstName=Elly, lastName=Doe, email=john.doe@example.com, birthDate=1990-05-15T00:00, sex=1, createBy=8b670765-5db2-4c78-8d90-f69bde7f799f, createDate=2024-12-19T17:06:41.172328, updateBy=null, updateDate=null)
c.e.p.controller.UserCacheController     : --------------------------------------------------

Send a request to clear the cache.

Postman clear cache
c.e.p.controller.UserCacheController     : Cache Name: users
c.e.p.controller.UserCacheController     : Cache is empty.
c.e.p.controller.UserCacheController     : --------------------------------------------------

The cache is empty when sending a request to clear the cache.

Conclusion

Spring Boot caching is a powerful feature that enables developers to optimize application performance with minimal effort. By leveraging annotations like @Cacheable@CachePut, and @CacheEvict, developers can seamlessly integrate caching into their applications. Whether you use in-memory solutions like EhCache or distributed systems like Redis, Spring Boot provides the flexibility to meet diverse caching needs.

Integrating caching strategically ensures efficient resource usage, faster response times, and better scalability — key factors for building modern, high-performing applications.

Leave a Comment

Your email address will not be published. Required fields are marked *