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).
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).
- 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. - 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.
Empirical load testing was conducted to measure system behavior under heavy load, evaluating throughput, error rate, JVM memory consumption, and Tomcat thread safety.
- 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)
| 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 |
-
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.
-
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.
βββ 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
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 viabrew install k6orchoco install k6) - cURL or Postman (for manual API verification)
Spin up PostgreSQL and Redis containers in detached mode:
docker-compose up -dVerify that both services are healthy:
docker-compose psor
docker psThe 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;
"Before spinning up the application, execute the test suite to verify database constraints and Redisson lock behavior:
./mvnw clean testThe 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 busyCompile and launch the Spring Boot service:
./mvnw spring-boot:runThe server will start listening on http://localhost:8080.
Once the server is running, access the OpenAPI/Swagger interface directly in your browser:
- Swagger UI: http://localhost:8080/swagger-ui/index.html
- OpenAPI JSON Spec: http://localhost:8080/v3/api-docs
-
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
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