Implement Caching in Spring Boot Application

Mohit Sehgal
6 min readMay 13, 2024

--

Today I will implement caching in spring boot application in a very simple way. But before we begin:

Please share, clap and follow for more information and demos. Put some comments and share your view on this article.

Do not forget to subscribe the mailing list.

I have taken a very simple example, I will be storing chemical names and its scientific formula in database. This can be a perfect example where we can implement caching.

When should I implement caching?

I should implement caching on the service layer that deals with data which is not modified frequently. Such data for example, formula of a chemical compound is never going to change or there is very less chance that the chemical formula of some compound will change.

Let me show you my implementation.

  1. pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.caching.demo.app</groupId>
<artifactId>caching-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>caching-demo</name>
<description>Demo project for Spring Boot Caching</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

2. Main Class:

package com.caching.demo.app;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class CachingDemoApplication {

public static void main(String[] args) {
SpringApplication.run(CachingDemoApplication.class, args);
}

}

We need to add @EnableCaching to enable caching in our application.

3. Entity:

package com.caching.demo.app.entity;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.boot.autoconfigure.domain.EntityScan;

@Entity
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChemicalCompoundEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(name = "chemical_name")
private String ccName;
@Column(name = "chemical_formula")
private String ccFormula;

}

This is entity for chemical compounds. It will store chemical name like “Water” in ccName and its formula H2O in ccFormula.

4. Repository:

package com.caching.demo.app.repository;

import com.caching.demo.app.entity.ChemicalCompoundEntity;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface CCRepository extends JpaRepository<ChemicalCompoundEntity,Long> {

Optional<ChemicalCompoundEntity> findByCcName(String ccName);
void deleteByCcName(String ccName);

}

5. Service:

package com.caching.demo.app.service;

import com.caching.demo.app.entity.ChemicalCompoundEntity;
import com.caching.demo.app.repository.CCRepository;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
@CacheConfig(cacheNames={"chemical_compounds"})
public class CCService {

@Autowired
private CCRepository repository;

@CachePut(value = "chemical_compounds", key="#root.args[0]")
public ChemicalCompoundEntity create(String ccName, String ccFormula) {
return repository.save(ChemicalCompoundEntity.builder().ccName(ccName).ccFormula(ccFormula).build());
}

@Cacheable(value = "chemical_compounds", key="#root.args[0]")
public ChemicalCompoundEntity getByName(String name) {
// I am directly using .get() without checking
// if optional container has it or not.
// This is not good but this is just a demo application
// so I will keep it as is.
return repository.findByCcName(name).get();
}

@CachePut(value = "chemical_compounds", key="#root.args[0]")
public ChemicalCompoundEntity updateFormula(String ccName, String ccFormula) {
Optional<ChemicalCompoundEntity> compound = repository.findByCcName(ccName);
if(compound.isPresent()){
ChemicalCompoundEntity chemComp = compound.get();
chemComp.setCcFormula(ccFormula);
return repository.save(chemComp);
}
return null;

}

@Transactional
@CacheEvict(value="chemical_compounds", key="#root.args[0]")
public void deleteCompound(String ccName) {
repository.deleteByCcName(ccName);
}

@CacheEvict(value="chemical_compounds", allEntries = true, key="#root.args[0]")
public void deleteAllCompound() {
repository.deleteAll();
}

}

This is interesting class, we have used so many annotations:

  • @CacheConfig: @CacheConfig provides a mechanism for sharing common cache-related settings at the class level. I have created cache with name: “chemical_compounds”.
  • @CachePut: The method with this annotation will always be called and the return value of the method will be cached. If your method is returning void then nothing will be stored in cache. It is never going to return result from cache, it is always goint to update existing cache. Key attribute “#root.args[0]” means the first argument passed to method. In my case, it is chemical name. I have used it as key because almost all the methods are taking it as parameter and we can easily identify uniquely by its compound name. Better to put this annotation on creation/updation methods. This annotation has features of conditional caching and the unless attribute like below:
//Cache only the value that is returned when comp is Water
@CachePut(value="chemical_compounds", condition="#comp.name=='Water'")
public String getFormula(ChemicalCompoundEntity comp) {...}

//If result length is less than 64 then it will not be cached.
@CachePut(value="chemical_compounds", unless="#result.length()<64")
public String getFormula(ChemicalCompoundEntity comp) {...}
  • @Cacheable: If the return value is already present in cache then it will be returned without executing the method. Better to keep this annotation on methods which will merely execute select query. If no value is found in the cache for the computed key, the target method will be invoked and the returned value will be stored in the associated cache. Note that Optional return types are unwrapped automatically. If an Optional value is present, it will be stored in the associated cache. If an Optional value is not present, null will be stored in the associated cache.
  • @CacheEvict: It will remove elements from the cache.

Internally, @EnableCaching would register ConcurrentMapCacheManager with IOC container.

My assumption is that cache is nothing but in-memory map ( may be concurrent Map) and that is why passing key is important so that exact values gets created/updated/removed.

6. RestController:

package com.caching.demo.app.controller;

import com.caching.demo.app.entity.ChemicalCompoundEntity;
import com.caching.demo.app.service.CCService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;


@RestController
public class CCRestController {

@Autowired
private CCService ccService;

@GetMapping("/create")
public ResponseEntity<String> create(@RequestParam("name") String ccName, @RequestParam("formula") String ccFormula){
ccService.create(ccName,ccFormula);
return ResponseEntity.ok("Chemical Compound added");
}

@GetMapping("/get")
public ResponseEntity<ChemicalCompoundEntity> getChemical(@RequestParam("name") String name){
return ResponseEntity.ok(ccService.getByName(name));
}

@PutMapping("/update")
public ResponseEntity<String> updateFormula(@RequestParam("name") String ccName, @RequestParam("formula") String ccFormula){
ccService.updateFormula(ccName,ccFormula);
return ResponseEntity.ok("Updated");
}

@DeleteMapping("/delete")
public ResponseEntity<String> delete(@RequestParam("name") String ccName){
ccService.deleteCompound(ccName);
return ResponseEntity.ok("Deleted");
}

@DeleteMapping("/delete-all")
public ResponseEntity<String> deleteAll(){
ccService.deleteAllCompound();
return ResponseEntity.ok("All Deleted.");
}
}

7. application.properties:

spring.application.name=caching-demo
server.port=9001

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

8. Output:

Hit the below GET endpoints one by one:

http://localhost:9001/create?name=Water&formula=H2O
http://localhost:9001/create?name=CarbonDiOxide&formula=CO2
http://localhost:9001/get?name=CarbonDiOxide

The first two endpoints will simply create new data in the database and in addition, the created data will be kept in cache because I am return saved entity in the methods and I have placed @CachePut over the method.

The third endpoint “/get” , it will not execute its methods as the service layer method is annotated with @Cacheable and the result will be returned from cache. First 2 endpoints created entries in cache. Last point simply returns the result from the cache if it is found otherwise it will go and check in database and then return and the will be placed in cache as well.

Logs: Only two insert statements and no select query is fired:

  • Now, hit below PUT endpoint to update the database and cache as well:

Update query will be fired and both DB and Cache will be updated:

  • Now we will again hit /get endpoint and this time we will get updated formula of water and no select query will be fired. This is how our interactions with DB are reduced and we achieve performance improvements.

No select query is fired as per logs.

Similarly, if I will hit delete endpoints then the values will be deleted from DB and cache both.

One advice would be to never use caching for methods returning findAll() without any limits.

Please find the code repository here.

If you find any mistakes in any of my articles then please put in comments.

Thanks a lot for going through this article. Please follow and subscribe to mailing list.

--

--