Modern web applications must respond quickly while serving many users at the same time. Repeating an expensive database query for every request increases latency, database load, and infrastructure cost.
Caching stores frequently requested data in a faster storage layer so later requests can reuse it. Spring Boot integrates with Redis through Spring Data Redis and Spring’s cache abstraction, making it straightforward to add caching to service methods.
Redis is an open-source, in-memory data store that supports strings, lists, sets, hashes, sorted sets, and other data structures. Because commonly accessed values are held in memory, Redis can serve many reads and writes with very low latency.
Redis can be used as a cache, session store, message broker, rate limiter, or temporary data store. In this tutorial, Redis is used as a shared cache while MySQL remains the persistent source of truth.
Redis reads data from memory, which is usually faster than executing the same query against a disk-backed database on every request. This can reduce response time for frequently requested data.
After the first request stores a result in Redis, subsequent requests can be served from the cache. This reduces repeated database queries and leaves database resources available for operations that require persistence.
Redis supports replication, high availability, and clustering. A shared Redis instance also allows multiple Spring Boot application instances to use the same cached values.
Redis supports several data structures, although Spring’s cache abstraction normally stores application values using cache keys and serialized values. Direct Spring Data Redis APIs can be used when an application needs Redis-specific data structures.
Spring’s cache abstraction lets service methods use annotations such as @Cacheable, @CachePut, and @CacheEvict. Application code can describe cache behavior without manually calling Redis for every method.
@Cacheable.@CachePut updates the cache and @CacheEvict removes stale entries.This example uses Spring Boot, Spring Web, Spring Data JPA, Spring Cache, Spring Data Redis, and MySQL:
<?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.3.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>redis-cache-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>redis-cache-demo</name>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<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-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Important: spring-boot-starter-cache enables Spring’s caching abstraction. spring-boot-starter-data-redis provides Redis connectivity and the Redis cache manager.
Add the following settings to src/main/resources/application.properties:
spring.application.name=redis-cache-demo
server.port=8080
# MySQL configuration
spring.datasource.url=jdbc:mysql://localhost:3306/yourdatabase
spring.datasource.username=root
spring.datasource.password=yourpassword
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Hibernate configuration
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
# Redis configuration
spring.data.redis.host=localhost
spring.data.redis.port=6379
# spring.data.redis.username=default
# spring.data.redis.password=your-redis-password
# Enable Redis-backed caching
spring.cache.type=redis
spring.cache.redis.time-to-live=10m
spring.cache.redis.cache-null-values=false
Run MySQL and Redis before starting the application. Redis uses port 6379 by default.
package com.example.rediscache.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
public User() {
}
public User(String name, String email) {
this.name = name;
this.email = email;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
package com.example.rediscache.repository;
import com.example.rediscache.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
}
The service below demonstrates the three most common cache operations:
@Cacheable reads from Redis first and caches a database result when the key is missing.@CachePut always executes the method and updates the matching cache entry.@CacheEvict removes the cache entry after a delete operation.package com.example.rediscache.service;
import com.example.rediscache.entity.User;
import com.example.rediscache.repository.UserRepository;
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 UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Cacheable(cacheNames = "users", key = "#id")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
@CachePut(cacheNames = "users", key = "#user.id")
public User saveUser(User user) {
return userRepository.save(user);
}
@CacheEvict(cacheNames = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
package com.example.rediscache.controller;
import com.example.rediscache.entity.User;
import com.example.rediscache.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
User user = userService.getUserById(id);
return user == null
? ResponseEntity.notFound().build()
: ResponseEntity.ok(user);
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
User savedUser = userService.saveUser(user);
return ResponseEntity.status(HttpStatus.CREATED).body(savedUser);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
package com.example.rediscache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
yourdatabase.localhost:6379.POST /users to save a user in MySQL.GET /users/{id} twice. The first request reads from MySQL and stores the result in Redis; the second can be served from Redis.DELETE /users/{id}. The matching Redis entry is evicted.curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name":"Alex","email":"alex@example.com"}'
curl http://localhost:8080/users/1
curl -X DELETE http://localhost:8080/users/1
A cache is not the source of truth in this example; MySQL remains the persistent store. Whenever data changes, update or evict the corresponding cache entry. Configure a TTL so values do not remain stale forever.
Cache consistency becomes more important when multiple application instances or other services can modify the same data. Define ownership, invalidation, and refresh rules before using Redis in production.
Monitor Redis memory usage, cache hit rate, evictions, command latency, connection errors, and application response times. Handle Redis failures deliberately: depending on the use case, the application may fall back to MySQL, return a controlled error, or temporarily disable caching.
Spring Data Redis and Spring’s cache abstraction make it easier to improve application performance without scattering Redis-specific code throughout the application. With @Cacheable, @CachePut, and @CacheEvict, an application can cache read-heavy service operations, update entries when data changes, and remove stale values while keeping MySQL as the persistent source of truth.
Improve application performance with Redis-backed caching and clear invalidation rules.