Spring Boot + Redis

A complete, production-ready guide to implementing Redis caching in a Spring Boot 3 API to improve performance of frequently accessed database records.

Spring Boot 3.xSpring Data RedisJava 17+MySQLLombok

Overview & Dependencies

This example demonstrates a clean approach to implementing Redis caching using Spring Boot 3 and Spring Data Redis. We use Spring's Cache Abstraction (`@Cacheable`, `@CacheEvict`, `@CachePut`) to minimize boilerplate code.

The Flow: When a GET request is made, the application first checks the Redis cache. If the data exists (Cache Hit), it is returned immediately. If not (Cache Miss), the data is fetched from the MySQL database, saved to Redis, and then returned. Subsequent requests will be served from the cache until the data is updated or deleted, which triggers a cache eviction.

pom.xml Dependencies

Add the required dependencies for Spring Web, Data Redis, Data JPA, MySQL, and Lombok.

pom.xml
<!-- Spring Boot Starters -->
<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>

<!-- Redis Caching -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- Database -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

Application Properties

Configure your MySQL database connection, enable Redis caching, and set the Redis host and port.

application.properties
# Database Configuration
spring.datasource.url=jdbc:mysql://localhost:3306/product_db?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

# Hibernate Settings
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# Redis Configuration
spring.data.redis.host=localhost
spring.data.redis.port=6379

# Enable Caching
spring.cache.type=redis

# Logging to see cache hits vs DB hits
logging.level.org.hibernate.SQL=DEBUG
logging.level.org.springframework.cache=TRACE

Note: Make sure you have a Redis server running locally on port 6379. You can use Docker: `docker run --name redis -p 6379:6379 -d redis`.

Entity & Repository

We define a `Product` entity. Note that the entity must implement `Serializable` for it to be cached in Redis.

entity/Product.java
package com.example.redis.entity;

import jakarta.persistence.*;
import lombok.*;

import java.io.Serializable;
import java.math.BigDecimal;

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

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(columnDefinition = "TEXT")
    private String description;

    @Column(nullable = false)
    private BigDecimal price;
}
repository/ProductRepository.java
package com.example.redis.repository;

import com.example.redis.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Redis Configuration

We configure the `RedisCacheManager` to serialize values as JSON (making them human-readable in Redis) and set a Default Time-To-Live (TTL). Don't forget `@EnableCaching` on this config or the main application class.

config/RedisConfig.java
package com.example.redis.config;

import org.springframework.cache.annotation.EnableCaching;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(10)) // Set cache expiration to 10 minutes
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
                .disableCachingNullValues();

        return RedisCacheManager.builder(connectionFactory)
                .cacheDefaults(config)
                .build();
    }
}

Caching Logic (Service Layer)

This is where the magic happens. We use `@Cacheable` to cache read operations, and `@CacheEvict` or `@CachePut` for write/update operations.

service/ProductService.java
package com.example.redis.service;

import com.example.redis.entity.Product;
import com.example.redis.repository.ProductRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
@Slf4j
public class ProductService {

    private final ProductRepository productRepository;

    public Product createProduct(Product product) {
        log.info("Creating new product in DB");
        return productRepository.save(product);
    }

    // Cache the result. If key exists, DB is not hit.
    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        log.info("Fetching product from DB for ID: {}", id);
        return productRepository.findById(id)
                .orElseThrow(() -> new RuntimeException("Product not found"));
    }

    // Cache the whole list. Note: updating a single product should evict this.
    @Cacheable(value = "products")
    public List<Product> getAllProducts() {
        log.info("Fetching all products from DB");
        return productRepository.findAll();
    }

    // Update DB and update the cache for this specific product.
    // Also evict the "products" cache list so the next get-all fetches fresh data.
    @CachePut(value = "product", key = "#id")
    @CacheEvict(value = "products", allEntries = true)
    public Product updateProduct(Long id, Product productDetails) {
        log.info("Updating product in DB for ID: {}", id);
        Product product = getProductById(id);

        product.setName(productDetails.getName());
        product.setDescription(productDetails.getDescription());
        product.setPrice(productDetails.getPrice());

        return productRepository.save(product);
    }

    // Delete from DB and remove from cache.
    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        log.info("Deleting product from DB for ID: {}", id);
        productRepository.deleteById(id);
        // We should manually or via another annotation evict "products" list here too
        // For simplicity, we can do it programmatically or add another @CacheEvict
    }
}

Product Controllers

The controller exposes REST endpoints and delegates to the caching service.

controller/ProductController.java
package com.example.redis.controller;

import com.example.redis.entity.Product;
import com.example.redis.service.ProductService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

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

    private final ProductService productService;

    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody Product product) {
        return new ResponseEntity<>(productService.createProduct(product), HttpStatus.CREATED);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProductById(@PathVariable Long id) {
        return ResponseEntity.ok(productService.getProductById(id));
    }

    @GetMapping
    public ResponseEntity<List<Product>> getAllProducts() {
        return ResponseEntity.ok(productService.getAllProducts());
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> updateProduct(@PathVariable Long id, @RequestBody Product product) {
        return ResponseEntity.ok(productService.updateProduct(id, product));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.noContent().build();
    }
}

Testing & Performance Logs

With the application running, let's observe the behavior of the cache hits versus database hits.

cURL Commands & Log Output

1. Create a Product
curl -X POST http://localhost:8080/api/products \
-H "Content-Type: application/json" \
-d '{"name":"Laptop", "description":"High performance laptop", "price":1500.00}'
2. Get Product (First Request = CACHE MISS)
curl -X GET http://localhost:8080/api/products/1

# LOG OUTPUT:
# Fetching product from DB for ID: 1
# Hibernate: select p1_0.id,p1_0.description,p1_0.name,p1_0.price from products p1_0 where p1_0.id=?
# Response time: ~150ms
3. Get Product Again (Second Request = CACHE HIT)
curl -X GET http://localhost:8080/api/products/1

# LOG OUTPUT:
# (No DB query logged. No "Fetching product from DB" logged!)
# Response time: ~5ms

Performance Improvement

By caching frequent read requests (like getting a product by ID), we bypassed the MySQL database and network latency entirely.

In the example logs above, the response time drops from ~150ms (fetching from DB and saving to Redis) to ~5ms (fetching directly from Redis). This drastically reduces database load and speeds up the API response.