Building Ultra-Fast APIs with Spring Boot 3.2 and Redis Caching

Introduction — Why Cache Matters

In modern microservice architectures, performance and scalability are not optional — they are the foundation. When APIs serve hundreds of requests per second, even milliseconds add up. Every database call, if repeated, can drain response time and system resources. That’s where caching comes in.


Caching means temporarily storing frequently accessed data in memory so that future requests can be served much faster without hitting the database.


Among various caching solutions, Redis stands out as:

  • Blazingly fast (in-memory store)
  • Easy to integrate
  • Open source and lightweight
  • Supports advanced data structures and pub/sub models


Here, we’ll build a real-world API using Spring Boot 3.2 and integrate Redis caching to achieve high performance with minimal code.

Real-World Use Case

Imagine you’re building a Service for an e-commerce platform.

  • The API fetches product details from the database.
  • Users often view the same products multiple times.
  • Without caching, every request hits the DB.
  • With caching, once a product is fetched, it’s stored in Redis — subsequent calls are served instantly.


This approach reduces:

  • Database load
  • API latency
  • Cloud compute cost

Architecture Overview

Flow Diagram

Project Setup

Step 1: Create a Spring Boot Project

Generate from https://start.spring.io


Dependencies

  • Spring Web
  • Spring Data JPA
  • Spring Data Redis
  • Lombok
  • H2 Database (for demo)


Use

  • Spring Boot: 3.2.x
  • Java: 17+
  • Build Tool: Maven


Folder Structure

springboot-redis-cache/
│
├── src/main/java/com/example/redis/
│   ├── controller/
│   │    └── ProductController.java
│   ├── entity/
│   │    └── Product.java
│   ├── repository/
│   │    └── ProductRepository.java
│   ├── service/
│   │    └── ProductService.java
│   ├── RedisCacheApplication.java
│
└── resources/
    ├── application.yml
    └── data.sql

Step-by-Step Implementation

Step 1 — The Product Entity

package com.example.rediscache.model;

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

@Entity
@Table(name = "product")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Double price;
    /**
     * @return the id
     */

    // Default constructor (required for JPA and deserialization)
    public Product() {}

    // All-arguments constructor
    public Product(Long id, String name, double price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public double getPrice() { return price; }
    public void setPrice(double price) { this.price = price; }
}


This simple entity represents a product stored in the database.

Step 2 — Repository

package com.example.rediscache.repository;

import com.example.rediscache.model.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

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

Step 3 — Service Layer with Caching

package com.example.rediscache.service;

import com.example.rediscache.model.Product;
import com.example.rediscache.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;

import java.util.List;

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

    @Autowired
    private ProductRepository repo;

    @Cacheable(value = "product", key = "#id")
    public Product getProductById(Long id) {
        System.out.println("[DB] Fetching product by id: " + id);
        return repo.findById(id).orElse(null);
    }

    @Cacheable(value = "allProducts")
    public List<Product> getAllProducts() {
        System.out.println("[DB] Fetching all products");
        return repo.findAll();
    }

    @Caching(put = {@CachePut(value = "product", key = "#result.id")}, evict = {@CacheEvict(value = "allProducts", allEntries = true)})
    public Product saveOrUpdate(Product product) {
        Product p = repo.save(product);
        System.out.println("[DB] Saved product: " + p.getId());
        return p;
    }

    @CacheEvict(value = "product", key = "#id")
    public void deleteProduct(Long id) {
        repo.deleteById(id);
        System.out.println("[DB] Deleted product: " + id);
    }
}

Key annotations explained

| Annotation | Purpose |
|—-|—-|
| @Cacheable | First checks Redis; if not found, calls DB and stores in cache |
| @CachePut | Updates both the DB and the cache simultaneously |
| @CacheEvict | Removes the entry from the cache when a record is deleted |
| @CacheConfig | Sets a common cache name for the service |

Step 4 — Controller

package com.example.rediscache.controller;

import com.example.rediscache.model.Product;
import com.example.rediscache.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

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

    @Autowired
    private ProductService service;

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        Product p = service.getProductById(id);
        if (p == null) return ResponseEntity.notFound().build();
        return ResponseEntity.ok(p);
    }

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

    @PostMapping
    public ResponseEntity<Product> create(@RequestBody Product product) {
        Product p = service.saveOrUpdate(product);
        return ResponseEntity.created(URI.create("/api/products/" + p.getId())).body(p);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody Product product) {
        product.setId(id);
        Product p = service.saveOrUpdate(product);
        return ResponseEntity.ok(p);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<String> delete(@PathVariable Long id) {
        service.deleteProduct(id);
        return ResponseEntity.ok("Deleted");
    }
}

Step 5 — Configuration

spring:
  cache:
    type: redis
  redis:
    host: localhost
    port: 6379
  datasource:
    url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password:
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: false

logging:
  level:
    root: INFO
    com.example.rediscache: DEBUG

Step 6 — Main Class

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 SpringbootRedisCacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootRedisCacheApplication.class, args);
    }
}

Step 7 — pom.xml

<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-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.32</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

Step-by-Step Validation

Step 1 — Start Redis

Run

redis-server


Keep it running. Verify in CMD

redis-cli ping


Output

PONG

Step 2 — Run the Spring Boot App

Run As → Java Application

Step 3 — Add Products

POST → http://localhost:8080/api/products
{
  "name": "MacBook",
  "price": 1699.99
}

POSTMAN

Console log

Fetching from DB for ID: 1


RedisInsight now shows a key:

products::4

REDIS

Step 4 — Fetch Cached Product

GET → http://localhost:8080/products/1


Output (same as before), but console log doesn’t show “Fetching from DB” — because it’s now served directly from Redis.


Cache Hit Successful!

Step 5 — Delete Product and Verify Eviction

DELETE → http://localhost:8080/products/1


Then check RedisInsight again — the key products::1 disappears.


Cache Eviction Successful!

How It Works

First Request

  • Cache is empty.
  • Data is fetched from the database.
  • Result stored in Redis under key products::1.

Next Requests

  • Fetched directly from Redis (milliseconds response time).

Update/Delete

  • @CachePut refreshes the cache after saving.
  • @CacheEvict removes outdated entries.

Observing Redis in Real-Time (Using RedisInsight)

Open RedisInsight (https://redis.io/insight)


Steps:

  1. Connect → localhost:6379

  2. Go to “Keys” tab. You’ll see cached entries like products::1.

    REDIS

  3. Double-click to view serialized data. You’ll see cached object details, including ID, name, and price.

    REDIS

  4. On deletion, it disappears immediately — proving cache eviction.

  5. You can set cache expiry to automatically refresh stale data

spring:
  cache:
    redis:
      time-to-live: 60000 # 60 seconds

Performance Comparison

| Operation Type | Without Cache (MySQL) | With Redis Cache |
|—-|—-|—-|
| First Fetch | ~120 ms | ~120 ms |
| Subsequent | ~5 ms | ~2 ms |
| Delete Product | 90 ms | 90 ms + Evict |

Result: ~60x faster on repeated reads!

Advantages of Redis Caching

| Benefit | Description |
|—-|—-|
| Blazing Fast | Data served from memory, not DB |
| Cost Efficient | Reduces DB reads & compute load |
| Smart Expiration | TTLs prevent stale data |
| Reusability | Cache layer works across services |
| Reliable | Redis supports clustering & persistence |

Real-World Scenarios

  1. Product Catalog/Inventory APIs → Cache product and pricing data for thousands of users.
  2. User Profile Data → Frequently accessed user details are cached for instant retrieval.
  3. Currency Conversion / Configurations → Cache frequently used lookups.
  4. Microservice Communication → Cache service responses to minimize inter-service latency.

Summary

By integrating Redis caching into your Spring Boot application

  • You offload database pressure.
  • You enhance user experience through faster responses.
  • You build APIs that can handle higher concurrency efficiently.

| Concept | Description |
|—-|—-|
| @Cacheable | Fetch from cache or DB if missing |
| @CachePut | Update cache with new value |
| @CacheEvict | Remove entry from cache |
| Redis TTL | Expire cache automatically |

One design pattern, one configuration — massive performance gains.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.