Skip to content

Latest commit

Β 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

High-Concurrency Spring Boot API: Cache Stampede Mitigation

Java Spring Boot Redis PostgreSQL Grafana k6 Docker

A production-ready Spring Boot 3 REST API demonstrating high-throughput performance optimization and Cache Stampede (Thundering Herd Problem) mitigation using Redisson Distributed Locks (RLock) and Redis Caching on PostgreSQL.

Includes an automated Grafana k6 load-testing suite simulating peak traffic bursts up to 1,000 Concurrent Virtual Users (VUs).


πŸ“Œ Problem & Architectural Solution

The Problem: Cache Stampede

When high-volume traffic hits an API simultaneously and a key cache entry expires, hundreds of concurrent requests bypass Redis and query the PostgreSQL database at the exact same millisecond. This leads to:

  • Database connection pool exhaustion.
  • CPU saturation & extreme latency spikes.
  • Cascading system failure (Thundering Herd).

The Solution: Distributed Locking (RLock)

  1. Single Database Fetcher: Using Redisson's RLock, only ONE thread acquires the lock upon cache invalidation to fetch fresh data from PostgreSQL and update Redis.
  2. Concurrent Readers Wait & Reuse: Concurrent threads wait briefly for the lock release and subsequently fetch the freshly populated data directly from Redis in single-digit milliseconds.

Load Testing Benchmark (Grafana k6)

Empirical load testing was conducted to measure system behavior under heavy load, evaluating throughput, error rate, JVM memory consumption, and Tomcat thread safety.

Workload Profile

  • Virtual Users (VUs): Ramping up to 1,000 Concurrent VUs
  • Total Requests Executed: ~1,000,000+ requests across the test suite
  • Embedded Tomcat Configuration: server.tomcat.threads.max=600 (Optimized Sweet Spot)

Key Metric Summary

Metric Result Engineering Status
Throughput (RPS) ~1,380 - 1,500 Req/sec 🟒 High Sustained Throughput
Success Rate 100.00% (0 Failed Req) 🟒 Zero Request Drop
Peak Latency (Max) 1.52 Seconds 🟒 Controlled Queueing
CPU Utilization 42% - 62% Peak 🟒 Healthy Headroom (~38% free)
JVM Heap Memory 100 MB - 300 MB 🟒 Healthy GC Pattern

Engineering Insights & Capacity Planning

  1. Tomcat Thread Tuning vs Operating System Cost:

    • Capping Tomcat threads at 600 threads proved to be the optimal throughput-to-resource sweet spot for a single monolithic instance.
    • Pushing thread count to 800–1,200+ increases OS Context Switching overhead and consumes unnecessary Thread Stack Memory (~1MB per thread) with diminishing throughput gains.
  2. Architectural Limit & Event-Driven Transition:

    • For traffic volume exceeding synchronous Web Server capacities (~1,500+ RPS), database/cache tuning yields minimal returns (<5%).
    • Recommendation: Transition asynchronous workload components into an Event-Driven Architecture (Apache Kafka / RabbitMQ) to offload thread-holding HTTP queues.

πŸ“‚ Repository Structure

β”œβ”€β”€ docker-compose.yaml
β”œβ”€β”€ HELP.md
β”œβ”€β”€ k6
β”‚Β Β  β”œβ”€β”€ 00-get-all-products.js
β”‚Β Β  β”œβ”€β”€ 01-unsafe-endpoint.js
β”‚Β Β  β”œβ”€β”€ 02-safe-endpoint.js
β”‚Β Β  β”œβ”€β”€ 03-combined-suite.js
β”‚Β Β  └── config.js
β”œβ”€β”€ mvnw
β”œβ”€β”€ mvnw.cmd
β”œβ”€β”€ pom.xml
β”œβ”€β”€ README.md
β”œβ”€β”€ src
β”‚Β Β  β”œβ”€β”€ main
β”‚Β Β  β”‚Β Β  β”œβ”€β”€ java
β”‚Β Β  β”‚Β Β  β”‚Β Β  └── com
β”‚Β Β  β”‚Β Β  β”‚Β Β      └── example
β”‚Β Β  β”‚Β Β  β”‚Β Β          └── demo
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”œβ”€β”€ common
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”‚Β Β  └── JsonUtil.java
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”œβ”€β”€ config
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”‚Β Β  β”œβ”€β”€ JacksonConfig.java
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”‚Β Β  └── RedissonConfig.java
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”œβ”€β”€ DemoApplication.java
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”œβ”€β”€ domain
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”‚Β Β  β”œβ”€β”€ presentation
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”‚Β Β  β”‚Β Β  └── ProductRepository.java
β”‚Β Β  β”‚Β Β  β”‚Β Β              β”‚Β Β  └── Product.java
β”‚Β Β  β”‚Β Β  β”‚Β Β              └── product
β”‚Β Β  β”‚Β Β  β”‚Β Β                  β”œβ”€β”€ dto
β”‚Β Β  β”‚Β Β  β”‚Β Β                  β”‚Β Β  β”œβ”€β”€ CreateProductRequest.java
β”‚Β Β  β”‚Β Β  β”‚Β Β                  β”‚Β Β  └── ProductResponse.java
β”‚Β Β  β”‚Β Β  β”‚Β Β                  β”œβ”€β”€ ProductController.java
β”‚Β Β  β”‚Β Β  β”‚Β Β                  └── ProductService.java
β”‚Β Β  β”‚Β Β  └── resources
β”‚Β Β  β”‚Β Β      β”œβ”€β”€ application.yaml
β”‚Β Β  β”‚Β Β      β”œβ”€β”€ static
β”‚Β Β  β”‚Β Β      └── templates
β”‚Β Β  └── test
β”‚Β Β      └── java
β”‚Β Β          └── com
β”‚Β Β              └── example
β”‚Β Β                  └── demo
β”‚Β Β                      β”œβ”€β”€ DemoApplicationTests.java
β”‚Β Β                      └── product
β”‚Β Β                          └── ProductServiceTest.java
└── target
    β”œβ”€β”€ classes
    β”‚Β Β  β”œβ”€β”€ application.yaml
    β”‚Β Β  └── com
    β”‚Β Β      └── example
    β”‚Β Β          └── demo
    β”‚Β Β              β”œβ”€β”€ common
    β”‚Β Β              β”‚Β Β  └── JsonUtil.class
    β”‚Β Β              β”œβ”€β”€ config
    β”‚Β Β              β”‚Β Β  β”œβ”€β”€ JacksonConfig.class
    β”‚Β Β              β”‚Β Β  └── RedissonConfig.class
    β”‚Β Β              β”œβ”€β”€ DemoApplication.class
    β”‚Β Β              β”œβ”€β”€ domain
    β”‚Β Β              β”‚Β Β  β”œβ”€β”€ presentation
    β”‚Β Β              β”‚Β Β  β”‚Β Β  └── ProductRepository.class
    β”‚Β Β              β”‚Β Β  └── Product.class
    β”‚Β Β              └── product
    β”‚Β Β                  β”œβ”€β”€ dto
    β”‚Β Β                  β”‚Β Β  β”œβ”€β”€ CreateProductRequest.class
    β”‚Β Β                  β”‚Β Β  └── ProductResponse.class
    β”‚Β Β                  β”œβ”€β”€ ProductController.class
    β”‚Β Β                  └── ProductService.class
    └── test-classes
        └── com
            └── example
                └── demo
                    β”œβ”€β”€ DemoApplicationTests.class
                    └── product
                        └── ProductServiceTest.class

Getting Started (End-to-End Guide)

Prerequisites

Make sure you have the following installed on your machine:

  • Java 17+ (java -version)
  • Docker & Docker Compose (docker compose version)
  • Grafana k6 (k6 version β€” install via brew install k6 or choco install k6)
  • cURL or Postman (for manual API verification)

1. Start Infrastructure (PostgreSQL & Redis)

Spin up PostgreSQL and Redis containers in detached mode:

docker-compose up -d

Verify that both services are healthy:

docker-compose ps

or

docker ps

2. Database Setup & Data Seeding

The application uses Spring Data JPA (hibernate.hbm2ddl.auto=update). Initial seed data is automatically injected on application startup via CommandLineRunner / data.sql.

If you prefer to manually seed data directly into the PostgreSQL container:

docker exec -i postgres_db psql -U postgres -d demo_db -c "
INSERT INTO products (id, name, stock, price, created_at) 
VALUES 
  (1, 'MacBook Pro M3', 500, 1999.99, NOW()),
  (2, 'iPhone 15 Pro', 1000, 999.99, NOW())
ON CONFLICT (id) DO NOTHING;
"

3. Run Unit & Integration Tests

Before spinning up the application, execute the test suite to verify database constraints and Redisson lock behavior:

./mvnw clean test

4. Tomcat Thread Configuration Location

The Tomcat thread pool max limit is explicitly tuned to prevent operating system context-switching bottlenecks under high concurrency.

You can review or adjust this configuration in src/main/resources/application.yml:

server:
  port: 8080
  tomcat:
    threads:
      max: 600        # Optimized sweet spot capped for 1,000 VUs
      min-spare: 20   # Minimum idle threads ready for incoming spikes
    accept-count: 200 # Connection queue capacity when all threads are busy

5. Run Spring Boot Application

Compile and launch the Spring Boot service:

./mvnw spring-boot:run

The server will start listening on http://localhost:8080.


6. Verify API Endpoints & Swagger Documentation

Interactive API Documentation (Swagger UI)

Once the server is running, access the OpenAPI/Swagger interface directly in your browser:

Manual API Verification

  • Hit Unsafe Endpoint (Without Distributed Lock - High DB Stress):

    curl -i -X GET http://localhost:8080/api/v1/products/unsafe/1
  • Hit Safe Endpoint (Protected by Redisson RLock & Redis Cache):

    curl -i -X GET http://localhost:8080/api/v1/products/safe/1

7. Run k6 Load Testing Benchmark

Execute the automated load-testing suite simulating 1,000 Concurrent Virtual Users (VUs):

# Run individual test for Safe Endpoint (Cache + Lock)
k6 run k6-tests/03-safe-endpoint.js
# Run full combined stress suite (Unsafe vs Safe Endpoints)
k6 run k6-tests/04-combined-suite.js

About

Spring Boot API demonstrating Cache Stampede mitigation using Redisson Distributed Locks & Redis. Benchmark tested under 1,000 VUs via k6.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages