Spring Boot + Redis + Docker: Ultimate Guide to Caching in Java
With Redis, your Spring Boot app can become faster and handle more traffic without breaking a sweat. This article will show you how to set it up step by step. However, before diving deep into Redis, we need to understand the concept of **caching**.
### **Cache**
Cache is a fast, small, temporary storage frequently used by the computer or application to store and access important data.
It stores data in a key-value format. By leveraging cache memory, we can minimize database calls, improving application performance since database queries are typically resource-intensive.
**The main objective of a cache is to speed up the retrieval of data by making a copy of the data in a location that can be accessed faster than the source or database.**
> **A cache is a small and fast, efficient memory space that an application frequently uses to store or access important data.**
### Why Caching?
The main objective of a cache is to speed up the retrieval of data by making a copy of the data in a location that can be accessed faster than the source or database.
In our application, whenever multiple requests access static data (data that is not changed frequently), we fetch the data from the database every time. Therefore, the number of database calls increases, which affects the performance of our application because database calls are always costly.
However, the static data can be stored in a cache, and whenever a request is made to access the data, it is fetched from the cache. As a result, the number of database calls is reduced, and the application's performance is improved.
### **How does a Cache work?**
In the diagram above, multiple requests made to access the data in the application will first check the cache to determine whether the data is present. If the data is found, it is returned from the cache, a concept known as a _Cache Hit_. If the data is not found, it is retrieved from the database, which is referred to as a _Cache Miss_.
### **Cache Hit**
**Data is found in the cache, so it has to be fetched from a faster source.**
We can understand the cache hit like this: imagine you have a notebook where you write down answers to questions you frequently ask. You ask a question, and the answer is already written in your notebook. you quickly find it without searching elsewhere.
### **Cache Miss**
**Data is not found in the cache, so it has to be fetched from a slower source.**
Similarly, you asked a question, but it’s not in your notebook. You have to search in a big textbook (which takes more time) and then write the answer in your notebook for future use.
## Redis
Redis (Remote Dictionary Server) is an open-source, in-memory key-value data store that supports various data structures, including strings, lists, sets, and hashes. Its in-memory architecture ensures high performance, making it an ideal choice for caching and session management in modern applications.
The **spring-boot-starter-redis** is a Spring Boot starter that simplifies integrating Redis into Spring applications. It includes all the necessary dependencies to connect, configure, and operate with Redis.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Boot provides the `spring-boot-starter-redis` starter, enabling seamless communication with the Redis server. It includes various components that facilitate efficient interaction with Redis.
* **JedisConnectionFactory** : Manages and establishes the connection between the Spring Boot application and the Redis server.
* **RedisTemplate** : It provides methods to perform operations like saving, retrieving, and deleting data in Redis.
* **StringRedisTemplate** : A specialized version of `RedisTemplate` that simplifies operations for String-based keys and values.
* **OpsForValue** : Supports operations on simple key-value pairs in Redis.
* **OpsForHash** : It is providing methods to peform operations based on Hash Data structure.
* **OpsForList** : Provides methods to interact with Redis Lists.
* **OpsForSet** : Facilitates operations on Redis Sets.
## Spring Boot with Redis Integration
Spring Boot provides seamless integration with Redis, an in-memory data store, through the `spring-boot-starter-redis` starter. It simplifies configuration and enables developers to use Redis for caching, messaging, and data persistence.
**Prerequisites for Running the Spring Boot Redis Integration Project**
* Redis Server (Local or Cloud-based; here we use docker)
* Java Development Kit (JDK 17 or above)
* Maven (Build tool)
* IDE (e.g., IntelliJ IDEA, Eclipse, or Spring Tool Suite)
* Postman (Optional, for testing REST APIs)
* Project Build Using Maven
### **Docker Redis Setup**
In this article, we install Redis using Docker. We can also manually download it from the Redis website, but here, we download the latest Redis image from Docker Hub (ensure Docker is already installed on your machine).
We have a sample spring-boot application that you clone from Github for initial setup. The initial code is available in the main branch, so please checkout to the main or redis integration branch. I also added the postman collection on the root directory of the project so you can test all the API before integration.
### **Steps to Integrate Redis with Spring Boot**
1. Add the Maven dependency in the `pom.xml` file. Since we are using Docker, we also include the Docker Compose dependency.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
</dependency>
1. Configure Redis Connection or database connection (we’re using H2 as database). Add the Redis server configuration to **`application.properties`** or **`application.yml`**
spring.application.name=spring-boot-redis-cache
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.cache.type=redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
# if we are using local redis or cloud but here we use docker so there is no need of username or password
spring.data.redis.username=
spring.data.redis.password=
1. Create the `docker-compose.yml` file on the root folder with the same naming convention we follow for docker-redis configurations.
services:
redis:
image: redis:7.4.2
ports:
- 6379:6379
1. Mark the spring-boot application class as `@EnableCaching` to enable caching in our Spring Boot application. To enable caching in our Spring Boot application, add the `@EnableCaching` annotation to one of our configuration classes. This annotation triggers a post-processor that inspects each Spring bean for caching annotations.
package com.ayshriv.springbootrediscache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class SpringBootRedisCacheApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRedisCacheApplication.class, args);
}
}
1. Create the class `RedisConfig.class` inside the config package
package com.ayshriv.springbootrediscache.config;
import com.techie.springbootrediscache.dto.ProductDto;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
@Configuration // Marks this class as a Spring configuration class
public class RedisConfig {
@Bean // Defines a Spring bean for RedisCacheManager
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
// Define cache configuration
RedisCacheConfiguration cacheConfig = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // Set time-to-live (TTL) for cache entries to 10 minutes
.disableCachingNullValues() // Prevent caching of null values
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new Jackson2JsonRedisSerializer<>(ProductDto.class))); // Serialize values using Jackson JSON serializer
// Create and return a RedisCacheManager with the specified configuration
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(cacheConfig) // Apply default cache configuration
.build();
}
}
This class configures Redis caching in a Spring Boot application. The `@Configuration` annotation marks it as a configuration class, and the `@Bean` annotation defines a `RedisCacheManager` bean.
The cache configuration includes a time-to-live (TTL) of 10 minutes (`Duration.ofMinutes(10)`), which ensures that cached data expires automatically after this period. It also disables the caching of `null` values (`disableCachingNullValues()`) to optimize memory usage.
For serialization, the `Jackson2JsonRedisSerializer` is used, which converts Java objects (`ProductDto`) to JSON before storing them in Redis. This ensures that cached data remains structured and readable.
The `RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(cacheConfig).build()` method initializes the cache manager with the given configuration and connects it to the Redis database using `RedisConnectionFactory`.
1. After doing all the configurations, we implement the caching on the business class (service class).
package com.ayshriv.springbootrediscache.service;
import com.techie.springbootrediscache.dto.ProductDto;
import com.techie.springbootrediscache.entity.Product;
import com.techie.springbootrediscache.repository.ProductRepository;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@CachePut(value="PRODUCT_CACHE", key="#result.id")
public ProductDto createProduct(ProductDto productDto) {
var product = new Product();
product.setName(productDto.name());
product.setPrice(productDto.price());
Product savedProduct = productRepository.save(product);
return new ProductDto(savedProduct.getId(), savedProduct.getName(),
savedProduct.getPrice());
}
@Cacheable(value="PRODUCT_CACHE", key="#productId")
public ProductDto getProduct(Long productId) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("Cannot find product with id " + productId));
return new ProductDto(product.getId(), product.getName(),
product.getPrice());
}
@CachePut(value="PRODUCT_CACHE", key="#result.id")
public ProductDto updateProduct(ProductDto productDto) {
Long productId = productDto.id();
Product product = productRepository.findById(productId)
.orElseThrow(() -> new IllegalArgumentException("Cannot find product with id " + productId));
product.setName(productDto.name());
product.setPrice(productDto.price());
Product updatedProduct = productRepository.save(product);
return new ProductDto(updatedProduct.getId(), updatedProduct.getName(),
updatedProduct.getPrice());
}
@CacheEvict(value="PRODUCT_CACHE", key="#productId")
public void deleteProduct(Long productId) {
productRepository.deleteById(productId);
}
}
When we mark the `createProduct()` method with this **`@CachePut(value = "PRODUCT_CACHE", key = "#result.id")`** annotation, this ensures that the object returned by this method is stored or updated in the cache name `PRODUCT_CACHE`, and the key part specifies that the cache key should be the ID of the returned `ProductDto` object.
This is useful in scenarios where a new product is created or an existing product is updated, as it ensures that the latest product details are stored in the cache. By using this annotation, subsequent requests for the same product can be served quickly from the cache, **reducing database queries and improving application performance** while keeping the cache up to date.
Next, when we mark the `getProductById()` method with this **`@Cacheable(value = "PRODUCT_CACHE", key = "#productId")`** annotation, this ensures that the returned object by this method is **stored in the cache** named `"PRODUCT_CACHE"`, and the `key` part specifies that the cache key should be the `productId` parameter passed to the method.
If the requested `productId` is already present in the cache, the method **skips execution** and directly returns the cached value, avoiding a database call. However, if the `productId` is not found in the cache, the method executes, retrieves the product from the database, **stores the result in the cache** , and then returns it.
When we mark the `deleteProduct()` method with this **`@CacheEvict(value = "PRODUCT_CACHE", key = "#productId")`** annotation, this ensures that the **cache entry associated with the given`productId` is removed** from the cache named `"PRODUCT_CACHE"`. The `key` part specifies that the cache key to be evicted is the `productId` parameter passed to the method.
This means that after deleting a product from the database, its cached entry will also be removed, ensuring that **stale data is not served from the cache** in future requests. If the deleted product is requested again, it will be fetched from the database and re-cached if applicable.
Here, we do all the things with the help of the annotation-based approach but we can also do the same things with the help of `CacheManager`.
The `CacheManager` is a **Spring framework interface** responsible for managing different cache implementations. It acts as a central mechanism to store, retrieve, and manage cached data efficiently.
private final CacheManager cacheManager;
public ProductDto createProduct(ProductDto productDto) {
var product = new Product();
product.setName(productDto.name());
product.setPrice(productDto.price());
Product savedProduct = productRepository.save(product);
Cache productCache = cacheManager.getCache("PRODUCT_CACHE");
productCache.put(savedProduct.getId(), savedProduct);
return new ProductDto(savedProduct.getId(), savedProduct.getName(),
savedProduct.getPrice());
}
## **Test the application**
1. ADD-PRODUCT: http://localhost:8080/api/product
2. GET-PRODUCT: http://localhost:8080/api/product/3
3. UPDATE PRODUCT: http://localhost:8080/api/product
4. DELETE PRODUCT: http://localhost:8080/api/product/2
## **Conclusion**
Redis with Spring Boot makes apps faster by caching data and reducing database calls. Using `@Cacheable`, `@CachePut`, and `@CacheEvict`, we can easily store, update, and delete cached data. This improves speed, reduces server load, and helps the app handle more users smoothly.
When we are performing the create operation, then the response of this operation is stored in the cache so whenever we perform operations like the `getProduct()` it checks the product in the cache first, if the product is present inside the cache, it returns that if not then it will return from the database.
Similarly, when we perform the delete operation, first it deletes the data from the database and then deletes it from the cache as well. All these types of operations reduce the number of DB calls so the performance of the application is increased.