Spring Boot Redis Cache Tutorial

Watch on YouTube

Spring Boot Redis Cache

Introduction

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.

What is Redis?

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.

Why use Redis as a Spring Boot cache?

1. Improved performance

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.

2. Reduced database load

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.

3. Scalability

Redis supports replication, high availability, and clustering. A shared Redis instance also allows multiple Spring Boot application instances to use the same cached values.

4. Flexible data structures

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.

5. Simple Spring Boot integration

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.

How Spring caching works

  1. A request calls a service method annotated with @Cacheable.
  2. Spring creates a cache key from the method arguments.
  3. If the key exists, Spring returns the cached value without calling the method body.
  4. If the key does not exist, the method reads from MySQL and Spring stores the result in Redis.
  5. When data changes, @CachePut updates the cache and @CacheEvict removes stale entries.

Create the Spring Boot project

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.

Configure MySQL and Redis

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.

Create the User entity

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;
    }
}

Create the repository

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> {
}

Use caching in the service

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);
    }
}

Create the REST controller

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();
    }
}

Enable caching in the application

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);
    }
}

Test the cache flow

  1. Start MySQL and create the database named yourdatabase.
  2. Start Redis on localhost:6379.
  3. Start the Spring Boot application.
  4. Send POST /users to save a user in MySQL.
  5. Send GET /users/{id} twice. The first request reads from MySQL and stores the result in Redis; the second can be served from Redis.
  6. Send 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

Cache consistency and expiration

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.

Monitoring and resilience

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.

Redis caching best practices

  • Set a TTL for cached values and choose it according to data volatility.
  • Evict or refresh entries whenever the underlying data changes.
  • Use stable, unique keys and include tenant or user scope when required.
  • Do not cache sensitive information without reviewing security and retention requirements.
  • Use a compatible serializer and test cache compatibility during deployments.
  • Monitor Redis and define a fallback strategy for outages.

Conclusion

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.

Continue learning