Give Your Spring Boot App a Turbo Boost with Redis Cache Provider — Because even code needs a little caffeine✨

Zeeshan Adil
Javarevisited
Published in
8 min readJan 7, 2024

In my previous article, I explained how to choose the right caching option for your Spring Boot application based on its specific requirements and business objectives. Please refer to that article for an overview of all available cache providers that work well with Spring Boot.

Spring Boot and Caching:

In the context of Spring Boot applications, caching is not just a performance optimization — it’s a fundamental feature. Leveraging the built-in caching support or integrating with popular caching libraries empowers developers to make informed choices based on their specific use cases and requirements.

In this article we will learn how to use redis as a cache provider to boost the application performance.

Overview of Redis at a Glance:

👉 A distributed, in-memory data store
👉 Spring Boot has good integration with Redis for distributed caching
👉Suitable for: Distributed caching, caching in microservices, real-time data updates, Message Broker (MQ)

The functioning of caching:

Here’s a simplified explanation of how caching typically works:

👉 Check if Data is in Cache:
Before performing a time-consuming operation or fetching data from the original source (like a database), the system checks if the required data is already present in the cache.

👉 Cache Hit:
If the data is found in the cache (a cache hit), it is directly retrieved from there. This avoids the need to execute complex and resource-intensive operations to fetch the data from the original source.

👉 Cache Miss:
If the data is not found in the cache (a cache miss), the system fetches the required data from the original source, such as a database or an API.

👉 Store Data in Cache:
After retrieving the data from the original source, the system stores a copy of it in the cache for future use. This helps to accelerate subsequent requests for the same data.

👉 Expiration and Eviction:
Cached data may have an expiration time to ensure that it remains up-to-date. Additionally, caches often have a limited capacity, and when the cache is full, a mechanism called eviction may be employed to remove less frequently used or stale data to make room for new entries.

Let’s start coding… 😊

📌 Project Structure as below:

📌 We need following dependencies in pom.xml

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.2.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

We included spring-boot-starter-data-redis maven dependency to set up Redis as a cache provider in spring boot application.

This starter adds support for using Redis as a caching provider. It includes the necessary dependencies and configurations to integrate Spring’s caching abstraction with Redis. When you include this starter, Spring Boot automatically configures a RedisCacheManager as the cache manager, allowing you to leverage Redis as a distributed cache.

The RedisCacheManager is responsible for creating and managing caches backed by a Redis server. It uses Redis to store cached data, making it suitable for distributed and scalable caching scenarios

📌 Configure Database and Redis in application.properties file

#datasource configs
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/redis_cache_db
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL8Dialect

#logging configs
logging.level.sql = debug
logging.level.org.springframework.data.redis=info

#redis configs
spring.cache.type=redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.cache.redis.time-to-live=6000

By default, Redis uses the following port and host:
Port: 6379
Host: localhost (127.0.0.1)
So, if you are connecting to a Redis server running on the same machine, you would typically use localhost as the host and 6379 as the port. If your Redis server is running on a different machine, you would use the appropriate IP address or hostname for the host.

🔥I set a Time To Live (TTL) of 6000 for a cache entry, it means that the corresponding cached data will be automatically removed after 6000 seconds (or 100 minutes) from the time it was originally cached. The TTL is a mechanism to control how long the data should be considered valid and retained in the cache.

📌 Enable the Caching by adding @EnableCaching annotation to the main class as below

@SpringBootApplication
@EnableCaching
public class RedisCacheProviderApplication {
public static void main(String[] args) {
SpringApplication.run(RedisCacheProviderApplication.class, args);
}
}

📌 Create a Product Model under models package:

@Entity
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "PRODUCTS")
public class Product implements Serializable {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "ID")
private Long id;
private String name;
private String category;
private double price;
private int quantity;

}

📌 Create ProductRepository under repositories package as below

@Repository
public interface ProductRepository extends CrudRepository<Product, Long> {
Product findFirstById(Long id);
}

📌 Create ProductService under servies package

public interface ProductService {

Product createProduct(Product product);
Product updateProduct(Product product);
Product getProductById(Long id);
List<Product> getAllProducts();
void deleteProduct(Long productId);

}

📌 Create ProductServiceImpl class under services package

package com.zees.redis.cache.services;

import com.zees.redis.cache.models.Product;
import com.zees.redis.cache.repositories.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Objects;
import java.util.Optional;

@Service
public class ProductServiceImpl implements ProductService{

@Autowired
ProductRepository productRepository;

@Override
public Product createProduct(Product product) {
return productRepository.save(product);
}

@Override
public Product updateProduct(Product product) {
if(!Objects.isNull(product) && product.getId() != null){
Product oldProduct = productRepository.findFirstById(product.getId());
oldProduct.setId(product.getId());
oldProduct.setName(product.getName());
oldProduct.setCategory(product.getCategory());
oldProduct.setPrice(product.getPrice());
oldProduct.setQuantity(product.getQuantity());
productRepository.save(oldProduct);
}
return null;
}

@Override
public Product getProductById(Long id) {
return productRepository.findFirstById(id);
}

@Override
public List<Product> getAllProducts() {
List<Product> products = (List<Product>) productRepository.findAll();
return products;
}

@Override
public void deleteProduct(Long productId) {
productRepository.deleteById(productId);
}
}

📌 Create ProductController class under controllers package

package com.zees.redis.cache.controllers;

import com.zees.redis.cache.models.Product;
import com.zees.redis.cache.services.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/v1")
public class ProductController {

@Autowired
ProductService productService;

@PostMapping(value = "/products/create")
public ResponseEntity<Product> createProduct(@RequestBody Product product){
try{
return ResponseEntity.ok(productService.createProduct(product));
} catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}

@PutMapping("/products/update")
@CachePut(cacheNames = "product", key = "#product.id")
public Product updateProduct(@RequestBody Product product){
try{
return productService.updateProduct(product);
} catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}

@GetMapping("/products/{id}")
@Cacheable(value = "product", key = "#id")
public Product getProductById(@PathVariable Long id){
try{
return productService.getProductById(id);
} catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}

@GetMapping("/products")
@Cacheable(value = "product")
public List<Product> getAllProducts(){
try{
return productService.getAllProducts();
} catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}

@DeleteMapping("/products/{id}")
@CacheEvict(cacheNames = "product", key = "#id", beforeInvocation = true)
public void deleteProduct(@PathVariable Long id){
try{
productService.deleteProduct(id);
} catch (Exception e){
throw new RuntimeException(e.getMessage());
}
}

}

🔥Let’s go through each endpoint in ProductController and explain the caching annotations used:

✏️Create Product Endpoint : POST /api/v1/products/create
👉 Purpose: This endpoint is used to create a new product.
👉 Caching Annotation: No caching annotation is used here. The result of creating a product is not cached, which means that every request to create a product will execute the createProduct method in the ProductService.

✏️ Update Product Endpoint: PUT /api/v1/products/update
👉 Purpose: This endpoint updates an existing product.
👉 Caching Annotation: @CachePut is used. After a successful update, the product with the updated state is put into the cache with the specified cache name “product” and the key being the ID of the updated product (“#product.id”).

✏️ Get Product by ID Endpoint: GET /api/v1/products/{id}
👉 Purpose: Retrieves a product by its ID.
👉 Caching Annotation: @Cacheable is used. It checks if the product with the given ID is already present in the cache. If found (cache hit), it returns the cached result; otherwise, it executes the method getProductById and caches the result with the specified cache name “product” and the key being the ID of the product (“#id”).

✏️ Get All Products Endpoint: GET /api/v1/products
👉 Purpose: Retrieves a list of all products.
👉 Caching Annotation: @Cacheable is used. It checks if the list of all products is already present in the cache. If found (cache hit), it returns the cached result; otherwise, it executes the method getAllProducts and caches the result with the specified cache name “product”.

✏️Delete Product Endpoint: DELETE /api/v1/products/{id}
👉 Purpose: Deletes a product by its ID.
👉 Caching Annotation: @CacheEvict is used. Before the actual deletion (beforeInvocation = true), it removes the product with the specified ID from the cache with the cache name “product”. After the deletion is successful, the cache is updated.

In addition to @Cacheable, @CachePut, and @CacheEvict, Spring Framework provides several other caching annotations that can be used to further enhance the caching behavior in your application.

✏️ Here are some notable ones:

@Caching: The @Caching annotation allows you to apply multiple caching annotations to a single method. This can be useful when you need a combination of caching behaviors for different aspects of a method.

@Caching(
cacheable = @Cacheable(value = "product", key = "#id"),
put = @CachePut(value = "product", key = "#result.id")
)
public Product getProductById(Long id) {
// ...
}

@CacheConfig: The @CacheConfig annotation is used at the class level to provide a common cache configuration for multiple methods within the class. It allows you to avoid repeating cache-related settings for each method.

@CacheConfig(cacheNames = "product")
@Service
public class ProductService {

@Cacheable
public Product getProductById(Long id) {
// ...
}

}

@CacheRemove: The @CacheRemove annotation is used to remove entries from the cache after a method execution. It is part of the JSR-107 standard.

@CacheRemove(cacheName = "productCache")
public void removeProductFromCache(Long id) {
// ...
}

We have done with coding. It’s time to test our application. 😊👏

👉To begin, initiate the Redis server. If Redis is not installed on your system, follow the steps outlined below:

📌 Visit the official Redis download page: https://redis.io/download.
📌 Choose the stable version, and download the appropriate version for your operating system. You might choose the tar.gz file for Unix-like systems or the MSI installer for Windows.
📌For Unix-like systems (Linux or macOS), you can use the following commands in the terminal to download and extract Redis:

Once you done with installation, navigate to the Redis installation directory and run the Redis server by using redis-server command as below:

👉 Now redis server is up and running. Start your spring boot application

👉 Execute CRUD operations via REST calls using Postman

  1. Monitor the console log to witness database queries being triggered during the initial REST calls.
  2. Repeat the CRUD operations and observe the console again. This time, you won’t notice any database queries as the data has been cached, and subsequent REST calls retrieve data from the cache. (wow… we implemented caching successfully 😊)

📂You can find the source code here…

Next to Learn 👇

A Comprehensive Guide to Choosing the Right Caching Option for Your Spring Boot Application

❤️ Support & Engagement ❤️

❤ If you find this article informative and beneficial, please consider showing your appreciation by giving it a clap 👏👏👏, highlight it and replying on my story story. Feel free to share this article with your peers. Your support and knowledge sharing within the developer community are highly valued.
Please share on social media
Follow me on : Medium || LinkedIn
Check out my other articles on Medium
Subscribe to my newsletter 📧, so that you don’t miss out on my latest articles.
❤ If you enjoyed my article, please consider buying me a coffee ❤️ and stay tuned to more articles about java, technologies and AI. 🧑‍💻

--

--