Spring Boot Caching

Shubham Wankhede
7 min readMar 26, 2023

--

caching is one of the most important aspect of any application because of which understanding how to work with cache should be our priority.

before we get started any further we should be aware about what is caching, when to use caching, what are the different consideration we have to make while caching, where exactly we want caching etc.

Caching : caching is a process of storing data in high speed temporary storage called as cache which helps us to allows the faster data access to improve the system/application performance.

so basically caching stores data in temporary memory so every time you don’t have to go through complex time consuming operations or perform the expensive operations to get the data, instead you can perform these operations one time and store the result in cache and can access them in much faster way next time

while deciding on caching we have to understand which type of caching we want i.e deciding on caching strategy

  1. Client Side Caching : storing data on clients browser such storing data in browser cache or local storage, client side caching can be used for storing web page elements.
  2. Server Side Caching : storing data on server side such in memory or on disk to reduce the server load and improve performance, server side caching can be used to store data which is shared across multiple request such as database queries.
  3. Read Aside Caching
  4. Read Through caching
  5. Write through caching

and lot more

there are lot of factors which we have to consider when we decide to go for caching because using caching, when used properly will improve the application performance and if not used properly it will reduce the performance instead of giving any benefits.

when understanding caching, you also have to understand the use case of caching in your application, different data access strategies, understand different eviction policies, understand different caching strategies like distributed caching, standalone caching etc and then choosing right cache provider which suites your application requirement for that I can recommend you below blog [here]

let’s understand how to cache the database results with an spring boot application caching with an example

In a Spring Boot application, database result caching can be used in various scenarios to improve application performance and reduce the load on the database. Here are some situations where database result caching can be beneficial:

  1. Frequently accessed data: If your Spring Boot application frequently accesses the same data, caching the database result can help reduce the number of queries and improve response times.
  2. Slow database queries: If you have slow or complex database queries that are executed frequently, caching the results can help reduce the load on the database and improve application performance.
  3. High traffic applications: If your Spring Boot application experiences high traffic, caching database results can help reduce the load on the database and improve response times.
  4. Limited database resources: If your database resources are limited, caching database results can help reduce the load on the database and improve overall application performance.
  5. Expensive data calculations: If your application performs expensive data calculations, caching the database result can help reduce the computation time and improve application performance.

When implementing database result caching in a Spring Boot application, it’s important to ensure that the cache is properly configured and updated to prevent data inconsistencies. Additionally, it’s important to monitor the cache to ensure that it’s functioning properly and not causing performance issues.

to work spring boot caching we have to use below dependency

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

Spring Boot Caching Annotation

spring boot provides us with some annotations to work with caching

  1. @EnableCaching
    to enable caching in spring boot application, we have use it on top of configuration class
  2. @Cachable(cacheNames="<cacheName>", key="<key>")
    this annotation we can use on top of method for which the we want to store result in cache and subsequent request when we pass same argument to method the result is returned from cache.
  3. @CachePut(cacheNames="<cacheName>", key="<key>")
    this annotation is used to update the cache without interrupting the method execution i.e. every time the method is called with @CachePut annotation the method is executed and result is placed in cache
  4. @CacheEvict(cacheNames="<cacheName>", key="<key>")
    this annotation is used to remove the data from cache

Caching Annotation Arguments

we can pass different argument to these annotation like cacheNames is used to specify the cache name and key is used to specify or identify the cache, we can also specify other arguments like condition to specify Cacheable condition, cacheManager , keyGenerator etc.

Key

key argument is used to assign identity of data

  • if one parameter is used for cacheable method the that argument is used as key
  • if no parameters are used for cacheable method then SimpleKey.EMPTY constant is used a key
  • if cacheable method has more than one argument then SimpleKey is used with all parameters

once you understand all above concepts let’s move towards understanding them with an example.

we are going to create a rest controller which performs some operation through service class where we are going to apply caching

UserRestController.java

@RestController
@RequestMapping("/users")
@Validated
public class UserRestController {

private UserService userService;

@Autowired
public UserRestController(UserService userService){
this.userService = userService;
}

@GetMapping("/{id}")
public ResponseEntity<User> findUserById(@PathVariable("id") String userId){
return new ResponseEntity<>(userService.getUserById(userId), HttpStatus.OK);
}

@GetMapping
public ResponseEntity<List<User>> findAllUsers(){
return new ResponseEntity<>(userService.getAllUsers(), HttpStatus.OK);
}

@PostMapping("/create")
public ResponseEntity<Void> createUser(@RequestBody @Valid User user){
userService.addUser(user);
return new ResponseEntity<>(HttpStatus.CREATED);
}

@PutMapping("/update/{id}")
public ResponseEntity<User> updateUser(@PathVariable("id") String userId, @RequestBody @Valid User user){
userService.updateUser(userId,user);
return new ResponseEntity<>(HttpStatus.OK);
}

@DeleteMapping("/delete/{id}")
public ResponseEntity<User> deleteUser(@PathVariable("id") @NotNull String userId){
userService.deleteUser(userId);
return new ResponseEntity<>(HttpStatus.OK);
}
}

you also need a service class to write the operations and then you can use caching annotations on top service class methods

@Service
@Slf4j
public class UserServiceImpl {

private UserRepository userRepository;
private CacheManager cacheManager;

@Autowired
public UserServiceImpl(UserRepository userRepository, CacheManager cacheManager){
this.cacheManager = cacheManager;
this.userRepository = userRepository;
}

@Cacheable(cacheNames = "users")
public User getUserById(String userId) {
log.info("getUserById : "+userId);
Optional<User> userOptional = userRepository.findById(userId);
if(userOptional.isEmpty()){
log.error("user not found : "+userId);
throw new UserNotFoundException(userId);
}
return userOptional.get();
}


public List<User> getAllUsers() {
log.info("getAllUsers");
List<User> users = userRepository.findAll();
if(users.isEmpty()){
log.error("users not found");
throw new UserNotFoundException();
}
return users;
}

public void addUser(User user) {
log.info("save user");
String userId = UUID.randomUUID().toString();
user.setId(userId);

userRepository.save(user);
}

@CachePut(key = "#userId", cacheNames = "users")
public void updateUser(String userId, User user) {
log.info("update user : "+userId);

Optional<User> userOptional = userRepository.findById(userId);
if(userOptional.isEmpty()){
log.error("user not found : "+userId);
throw new UserNotFoundException(userId);
}

User u = userOptional.get();

BeanUtils.copyProperties(user,u);
u.setId(userId);

userRepository.save(u);
}

@CacheEvict(key = "#userId", cacheNames = "users")
public void deleteUser(String userId) {
log.info("delete user");

if (!userRepository.existsById(userId)){
throw new UserNotFoundException(userId);
}

userRepository.deleteById(userId);
}
}

if you where test this application you will observe that cacheable methods execute the method for one time only when passed same arguments i.e. on first call it execute the method and stores the result in cache with argument userId as key in cache and for second request with same parameter the data is fetched from cache using userId as key

Note : it’s usually best practice to not apply caching on save and findAll method unless specifically required

if we use caching on save method and that record is never used it’s going to consume the data in cache, also if saved data is frequently updated then data may become quickly outdated

if we use caching on findAll method it’s going to load all data of table which really unnecessary unless the data is frequently used, it will not create any issue in testing since data is small but in production where data is large it is really not suitable to store all data, although we can use caching with pagination in certain scenarios when pagination record is used very frequently

let’s understand few things regarding the performance of cache once you are familiarize with basic caching operation

Understanding Cache Performance

Cache Hit
whenever you make a call to caching enabled method let’s say with ID as argument and your cache is able to find the record in cache with ID as key then it is called as cache hit

Cache Miss
whenever you make a call to caching enabled method let’s say with ID as argument and your cache is not able to find the record in cache with ID as key it is called as cache hit

whenever you are working with any cache it is our responsibility to monitory the performance of the cache, they key factors to understand about the performance of cache is

  1. cache size : size of the cache plays very important role in caching if cache size is too small it will lead to frequent cache data eviction on other hand if size is too large it will take large memory and will impact the overall system performance
  2. Eviction Policy : when cache is full, the system has to decide which data to evict to make space for new data, there are several eviction policies to choose, some of them are
    - LRU (Least Recently Used)
    - LFU (Least Frequently Used)
    - MRU (Most Recently Used)
    - TTL (Time to live)
  3. Cache Hit Rate and Cache Miss Rate : cache should have high hit rate meaning data is highly available instead of making call to underlying database and cache low miss rate as possible, a high miss rate indicates cache is not working properly
  4. Cache Placement : placement of the cache i.e either to client side or server side also impact the cache performance
  5. Cache Coherence : if multiple caches are used in distributed system it is important to ensure that they remains consistent with each other, this can be achieved by cache invalidation and cache synchronization

along with understanding the different peformance factors of caching we have to regularly monitor the cache to identify any issues or bottlneck, You can use various tools and techniques to monitor the cache, such as cache statistics, logging, and profiling.

Best Practices to follow for caching

  1. configure caching properties like cache timeout, maximum cache size, eviction policies etc these properties help you optimize the application and helps to improve the performance
  2. user appropriate cache keys, key should unique and identifiable, using appropriate cache key helps for cache collision and improves the hit rate
  3. monitoring the cache performance will help finding any performance issues and optimize cache behaviour
  4. try to maintain high hit rate
  5. try to use cache as much as possible for read only operations to prevent inconsistency, if cache data is updated frequently caching may not provide any benifits and may even cause data inconsistency issue.
  6. if your cache requires high availability and scalability try using distributed cache like redis, hazelcast.

--

--