Slow performance is a recurring problem that developers often face in the enterprise world. One of the most common approaches to addressing such a problem is caching.
This mechanism allows for achieving a substantial improvement in the performance of any type of application.
But, the problem is that dealing with caching is a challenging task.
Luckily, Spring Boot provides caching transparently (thanks to the Spring Boot Cache Abstraction), which allows consistent use of various caching methods with minimal impact on the code.
SQL
/ Docker
/ Java
/ Spring
/ Shell
.In this post, you will find everything you need to know to set up and use the cache mechanism in a spring boot project.
What is Caching
Caching is the mechanism aimed at enhancing the performance of any kind of application. So, caching is the process of storing and accessing data from a cache. Dealing with caching is complex, but understanding this concept is absolutely necessary for every developer.
If you want to understand the inner workings of caching, I have written another blog post about it below β
Why should we use caching?
In real word scenario, neither users nor developers want the application to take a long time to process requests. As developers, we would be proud of deploying the most performing version of our applications.
And as an end user, we are willing to wait only for a few seconds, and sometimes even milliseconds. In both cases, no one loves wasting their time looking at loading messages.
The more technical aspect of caching is that it allows us to avoid new requests or reprocessing data every time. Avoiding making new requests reduces the overall number of requests needed, which can decrease the cost of your infrastructure.
Caching with Spring Boot
To illustrate how caching works with Spring, I will use a simple Spring Boot demo project, you can also find it here on my GitHub.
So how this app work? We use all these components.
src
β main
β java
β com.example.demo
β controller
β ProductController # where we define Rest API Requests
β entity
β Product # entity need to be managed by JPA
β repository
β ProductRepository # manage data in Spring Boot Application
β runner
β AppRunner # run when app start to load bulk datas
β service
β ProductService # where we define logic before managing entity in repository
β DemoApplication # starting point of our app
β resources
β application.yml # where we define configs for our app
.env # use for setting environment variables
pom.xml # dependency management file
spring.dockerfile # docker file of our app
docker-compose.yml # here we define all the services need to be started
run-backend-service.sh # bash script to launch the app in automation
Dependencies
Here are the pom.xml
Maven dependencies used in this demo project
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
</dependencies>
Configuration
In the application.yml
we define properties required to make our project work
spring:
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://postgres-db:5432/productdb
username: postgres
password: something
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
logging:
level:
org.springframework: info
org.hibernate: info
server:
port: 8080
Entity
Create a Product entity class
@Getter
@Setter
@Entity
@Table(name = "product")
public class Product {
public Product() {}
public Product(String description, int price) {
this.description = description;
this.price = price;
}
@Id
@Column(nullable = false)
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private int id;
@Column(name = "description")
private String description;
@Column(name = "price")
@Min(0)
private int price;
}
Repository
In the repository class, we implement the JPA Repository
@Repository
public interface ProductRepository extends JpaRepository<Product, Integer> {}
Service
In the service class, we define all the logic related to the Product
entity.
In the official documentation on Spring Boot, you can find further information about those annotations.
@Service
@RequiredArgsConstructor
public class ProductService {
private final ProductRepository repository;
@Cacheable("products")
public List<Product> getAllProducts() {
return repository.findAll();
}
@Cacheable(value = "products", key = "#id")
public Product getProductById(int id) {
return repository.findById(id).orElseThrow(EntityNotFoundException::new);
}
@CachePut(value = "products", key = "#product.id")
public Product updateOneProduct(Product product) {
return repository.save(product);
}
@CacheEvict(value = "products", key = "#id")
public void deleteOneProduct(int id) {
repository.deleteById(id);
}
@CacheEvict(value = "products", allEntries = true)
public void deleteAllProducts() {
repository.deleteAll();
}
}
Controller
Here you define different routes for your project
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {
private final ProductService service;
@GetMapping("/all")
public ResponseEntity<List<Product>> getAllProducts() {
return new ResponseEntity<>(service.getAllProducts(), HttpStatus.OK);
}
@GetMapping("/{id}")
public ResponseEntity<Product> getOneProduct(@PathVariable("id") int id) {
return new ResponseEntity<>(service.getProductById(id), HttpStatus.OK);
}
@PutMapping("/update")
public ResponseEntity<Product> updateOneProduct(@RequestBody Product product) {
return new ResponseEntity<>(service.updateOneProduct(product), HttpStatus.OK);
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.ACCEPTED)
public void deleteOneProduct(@PathVariable("id") int id) {
service.deleteOneProduct(id);
}
@DeleteMapping()
@ResponseStatus(HttpStatus.ACCEPTED)
public void deleteAllProducts() {
service.deleteAllProducts();
}
}
Main class
In the main class, you have to add @EnableCaching
annotation to enable in the application
@EnableCaching
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
AppRunner
With the AppRunner
, we define a set of data in the database when the app starts so that we can test our request-response time with cache implemented.
@Component
@RequiredArgsConstructor
public class AppRunner implements CommandLineRunner {
private static final int NUMBER_OF_PRODUCTS = 25;
private final ProductRepository productRepository;
@Override
public void run(String... args) {
int numberOfProducts = NUMBER_OF_PRODUCTS;
while (numberOfProducts > 0) {
productRepository.save(new Product("Product " + numberOfProducts, 10 + numberOfProducts));
numberOfProducts--;
}
}
}
Dockerfile Spring Boot
Here we set the different steps needed to build the image of our Spring Boot app
FROM openjdk:19-alpine
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
Docker Compose
Here, we define all the services needed to start our backend app
version: '3.8'
services:
spring-app:
build:
context: .
dockerfile: spring.dockerfile
container_name: spring-caching
restart: always
ports:
- '8080:8080'
depends_on:
- postgres-db
postgres-db:
image: 'postgres:15-alpine'
restart: always
container_name: postgres-15
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=something
- POSTGRES_DB=productdb
ports:
- '5432:5432'
volumes:
- 'db:/var/lib/postgresql/data'
volumes:
db: null
Automation Script
Once we have defined all the services, the next step is to define an automation script that we will use to launch our services by avoiding to type the same commands again and again.
# stop previous containers
docker stop spring-caching postgres-15
# remove previous containers
docker rm spring-caching postgres-15
# regenerate source files after recent changes
./mvnw clean package -DskipTests
# rebuilt the docker services by ignoring the cache
docker compose build --no-cache
# launch the services in background
docker compose up -d
# log the status of the services
last_command_status=$(echo $?)
if [ "$last_command_status" -eq 0 ]
then
echo "All the service are up"
else
echo "Something went wrong..."
fi
Now we can run our script at the root folder of our project with this command
$ ./run-backend-services.sh
Once the all our service are up and running, we can go to one SQL (of your choice) console, and verify that we have all the dummy data in our database. So, you have to run this command in your console
SELECT * FROM product;
Then you will have the dummy data as a result of this query.
Now if you perform the GET
request to return all products for the first time, then you will see that the time is a bit long .
But! If you run the command for the second time, you will see that it takes less time to return the data.
Thatβs the Cache in action!
π¬ Open the demo video in full screen
I hope you enjoyed reading this, and I'm curious to hear if this tutorial helped you. Please let me know your thoughts below in the comments. Don't forget to subscribe to my newsletter to avoid missing my upcoming blog posts.
You can also find me here LinkedIn β’ Twitter β’ GitHub or Medium
Wrapping
In this article, we have looked at interesting things about Caching, and how to use it efficiently. I'm not going to add anything more because I think I've covered most of the important points on this topic. What to remember?
- Challenges of using caching
- Types of caching
- Caching annotation with Spring Boot
- A complete demo project here.