DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Evolving Spring Boot APIs to an Event-Driven Mesh
  • A Transaction-Grade Performance Blueprint for Spring Boot FinTech Microservices (Tracing, Histograms, and Kubernetes)
  • Caching Mechanisms Using Spring Boot With Redis or AWS ElastiCache

Trending

  • Building a High-Throughput Distributed Sequence Generator Using the Hi-Lo Algorithm
  • The Hidden Cost of AI Tokens: Engineering Patterns for 10x Resource Efficiency
  • How SaaS Architectures Break at Scale — and the Engineering Decisions That Prevent It
  • Build a GitHub Slack Bot With AWS Bedrock and MCP, Part 2
  1. DZone
  2. Software Design and Architecture
  3. Performance
  4. Optimizing High-Volume REST APIs Using Redis Caching and Spring Boot (With Load Testing Code)

Optimizing High-Volume REST APIs Using Redis Caching and Spring Boot (With Load Testing Code)

Cache reads with Redis, use @CachePut for write-through consistency, and prevent stampedes with distributed locks, then prove it works under load with JMeter.

By 
Mallikharjuna Manepalli user avatar
Mallikharjuna Manepalli
·
May. 18, 26 · Analysis
Likes (0)
Comment
Save
Tweet
Share
1.4K Views

Join the DZone community and get the full member experience.

Join For Free

High-volume REST APIs can easily become bottlenecked by database access, leading to high latency and poor throughput. Even after optimizing SQL queries and adding indexes, a database call might take hundreds of milliseconds, still far slower than a competitor’s 50 ms response that leverages caching. In-memory caching offers orders of magnitude faster data access. Traditional databases measure response times in milliseconds, while Redis operations complete in microseconds. 

By storing frequently accessed data in memory, APIs can handle dramatically more requests per second with much lower latency. As an example, one test showed that using Redis cut an expensive request’s response time from over 10 seconds down to under 1 second.

Setting Up Redis Caching in Spring Boot

Before diving into patterns, let’s ensure the basic setup is in place. We assume you have a local Redis server running. In your Spring Boot project, include the necessary dependencies for caching and Redis integration. For example, add the following to your Maven pom.xml:

XML
 
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.1.5</version>
</dependency>


These bring in Spring’s generic caching support and the Redis connector. Next, enable caching in your application by annotating a configuration or main class with @EnableCaching. Spring Boot will auto-configure a RedisCacheManager if it finds Redis on the classpath. You can then define cache settings via configuration. For example, you might set a default time to live for cache entries in application.properties or via a RedisCacheConfiguration bean. A simple property-based configuration for a local Redis could be:

Properties files
 
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.cache.redis.time-to-live=600000  # 600000 ms = 10 minutes TTL


Now we have a basic cache setup. Let’s explore caching patterns and how to implement them in Spring Boot.

Write-Through and Write-Behind Caching

Caching isn’t just for reads; we also need a strategy for writes. Write-through and write-behind are patterns to handle data modifications in a cached system:

Write-Through

On every data write, the application synchronously writes to the database and the cache. This ensures the cache is always up-to-date with the latest data. In practice, a write-through approach might perform the database operation, then immediately update the Redis cache with the new value. Spring’s caching abstraction can support this via annotations like @CachePut or by combining a normal save method with a manual cache update. For example, in a product service, we might do:

Java
 
@CachePut(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
    // Save to DB first
    Product saved = repo.save(product);
    return saved; // Spring will put this return value into "products::[id]" cache
}


This method will update the database and also put the new product data into the cache under the given key. The next read for that product can be served from cache immediately, with no stale data. If we delete an item, we can use @CacheEvict to remove it from the cache at the same time as removing it from the DB, preventing ghost entries.

Write-Behind (Write-Back)

In this less common strategy, the application writes to the cache first and defers the database write till later. The idea is to batch or coalesce many writes to reduce DB pressure.

Avoiding Cache Stampede (Thundering Herd)

When caching for high-volume traffic, cache stampedes are a serious concern. A stampede occurs when a cache entry expires or is missing, and many concurrent requests attempt to fetch the same data from the database at once. In a high QPS system, this can overwhelm the database and essentially negate the benefit of caching. We need strategies to prevent dozens or hundreds of threads from piling onto the DB when a popular item cache invalidates.

One common solution is to use locking or synchronization around cache misses. The idea is to ensure only one thread does the expensive database fetch and populates the cache, while the others wait or get served a stale value. In a single-instance application, you might synchronize on a Java lock per key. In a distributed environment, you’ll want a distributed lock. Redis itself can be used to implement this.

For our Spring Boot application, we could integrate Redisson and use it in the service method. For instance:

Java
 
RLock lock = redissonClient.getLock("lock:product:" + productId);
boolean acquired = lock.tryLock(5, 10, TimeUnit.SECONDS); // wait up to 5s to acquire, auto-release after 10s
if (acquired) {
    try {
        // Double-check cache after acquiring lock
        Product cached = redisTemplate.opsForValue().get(cacheKey);
        if (cached != null) {
            return cached;
        }
        // Cache still empty, fetch from DB and update cache
        Product dbData = repo.findById(productId);
        redisTemplate.opsForValue().set(cacheKey, dbData, Duration.ofMinutes(10));
        return dbData;
    } finally {
        lock.unlock();
    }
} else {
    // Could not acquire lock (timed out) – fallback to a stale cache or return an error
    ...
}


In the above pseudocode, multiple threads hitting a missing cache key will attempt to tryLock. One will succeed and do the DB query, while others wait up to 5 seconds. Once the first thread populates the cache and releases the lock, the others will find the data in the cache and avoid hitting the DB. This approach effectively serializes the cache miss for a given key, preventing a herd of concurrent DB calls. It’s a bit heavy, so you might not use it for every key; typically, you'll use it for very hot items or expensive queries that you know could trigger stampedes. Simpler techniques can also mitigate stampedes, like cache early recomputation or using slightly randomized TTLs so not everything expires at the same time.

Load Testing the Impact of Caching With JMeter

After implementing Redis caching, it’s critical to verify the performance improvements under realistic load. Apache JMeter is a popular tool for simulating concurrent users and measuring response times and throughput of your API. We can use JMeter to compare the API’s behavior with and without cache and ensure that our caching does indeed handle high volume as expected.

For example, suppose we want to test an endpoint /products/{id} which we’ve optimized with caching. We can create a JMeter test plan with a Thread Group of, say, 100 threads and loop them to send requests for various product IDs. JMeter will report metrics like average response time, throughput, error rate, etc. In a baseline test, you might observe higher latencies and lower throughput. Then, in a test with the cache warmed (most requests hitting the cache), you should see a dramatic reduction in response time and the ability to handle more requests per second. In one real-world inspired demo, using Redis caching improved latency from 10 seconds on a cold miss to under 1 second on subsequent hits. Another way to look at it: memory caching can serve data so fast that your throughput might be an order of magnitude higher than relying solely on the DB. This aligns with the earlier statement that no amount of DB tuning beats data served from an in-memory cache.

Using JMeter

Set up JMeter (you can run it in GUI mode to design the test plan, and then use non-GUI mode for the actual high-load run for better accuracy). Configure an HTTP Request sampler pointing at your API (e.g., GET http://localhost:8080/products/1234). Use a Thread Group to simulate the desired number of concurrent users and iterations. You can add a Timer if you want a delay between requests, or just hammer the API as fast as possible to find its max throughput. Add listeners like Summary Report or Aggregate Report to gather results.

To automate performance testing, you can even integrate JMeter with your build. A Maven plugin exists to run JMeter tests as part of a build pipeline.

JMeter Configuration Snippet

Suppose we want to quickly run a load test from the command line (non-GUI). We could use a command like:

Shell
 
jmeter -n -t path/to/testplan.jmx -l results.jtl -Jthreads=100 -Jduration=60


This would run the JMeter test plan for 60 seconds with 100 threads, logging results to results.jtl. Make sure to monitor your system while testing, especially if everything is on the same machine; the load test could itself become a bottleneck or interfere with results if not planned carefully.

As a quick check, you can also use Spring Boot Actuator metrics or Redis monitoring to see cache hit rates. A healthy caching layer under load should show a high cache hit percentage, which correlates with lower DB usage and faster responses.

Conclusion

Optimizing a high-volume REST API often requires rethinking data access patterns, and Redis caching is a powerful technique to achieve massive performance gains. By using the cache-aside pattern, we serve most reads from fast in-memory storage, drastically reducing latency and database load. With write-through strategies and careful cache invalidation, we keep cached data consistent with the source of truth. It’s equally important to anticipate real-world issues like cache stampedes using locks or other techniques to prevent cache misses from overwhelming your database in a traffic surge.

Finally, always test under load. Use tools like JMeter to simulate concurrent access and measure the impact of your caching. You should observe significant improvements in throughput and response times, validating that the cache is doing its job. If the results aren’t as expected, that’s an indication to refine your caching strategy or investigate bottlenecks.

REST Redis (company) Spring Boot Performance

Opinions expressed by DZone contributors are their own.

Related

  • Stateless JWT Auth Microservice Architecture With Spring Boot 3 and Redis Sentinel
  • Evolving Spring Boot APIs to an Event-Driven Mesh
  • A Transaction-Grade Performance Blueprint for Spring Boot FinTech Microservices (Tracing, Histograms, and Kubernetes)
  • Caching Mechanisms Using Spring Boot With Redis or AWS ElastiCache

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook