Fine-Tuning of Spring Cache
Caching is a fundamental concept for making web applications faster and more scalable. In the following, I explain how to configure and optimize Spring caching.
Join the DZone community and get the full member experience.
Join For FreeCaching is one of the most effective techniques for improving the performance of modern Spring applications. Especially in microservice architectures or high-traffic APIs, a well-configured cache can significantly reduce database load and greatly improve response times. By storing frequently accessed data in memory, applications can avoid repeated expensive operations such as database queries or external API calls. This article provides a compact yet comprehensive overview of Spring’s caching capabilities.
For example, without caching, each request follows this process:
Client → Web Server → Application → Database
When 10,000 users request the same data, without caching, this results in 10,000 database queries, high latency, and significant server load. With caching:
Client → Web Server → Application → Cache → Database
The first request hits the database, and the result is stored in the cache. Subsequent requests are served directly from the cache. This reduces database load, speeds up responses, and enhances scalability.
Caching can be implemented at multiple levels within a web application:
- Browser Cache
The browser stores static files such as images, CSS, and JavaScript. This prevents these files from being downloaded on every request, reducing load time and network usage. - CDN Cache
A Content Delivery Network (CDN) caches content across multiple servers geographically. Examples include Cloudflare and Akamai. This reduces latency for users worldwide and improves page load speed. - Server / Application Cache
Data is stored within the application or server to speed up access to frequently requested information. Typical examples include database query results, API responses, or computational results. Common technologies for this level include Redis, Ehcache, Caffeine, and Apache Ignite.
Spring Cache
The caching module in the Spring Framework implements precisely this application-level cache. Spring provides an abstraction layer that allows developers to define caching very easily using annotations. Spring handles cache access, storage, and invalidation, so the developer only needs to specify which methods should be cached.
Architecture
Typical architecture:
Client
↓
Controller
↓
Service (@Cacheable)
↓
Cache (Redis / Caffeine / Ignite)
↓
Database
-------
Controller
│
▼
Spring Proxy
│
▼
CacheInterceptor
│
▼
CacheManager → Cache Lookup
│
├── Cache Hit → Return Value
│
└── Cache Miss
│
▼
Service Method
│
▼
Cache Put
│
▼
Return Value
Spring sits between the service and the underlying cache system, providing a caching API that enables significant performance improvements. Spring defines the caching logic, while a cache provider handles data storage. The Spring caching abstraction offers several benefits: simple implementation via annotations, pluggable cache providers, reduced database load, and improved performance. Supported providers include Redis, Ehcache, Caffeine, and Apache Ignite.
Spring caching is typically built on proxy-based Aspect-Oriented Programming (AOP). A proxy object is placed between the caller and the service method. The simplified flow looks like this:
Controller → Proxy → Cache Check → Service Method
If a cache hit occurs, the actual service method is not executed; instead, the proxy returns the cached value immediately.
Remarks: Spring supports two types of proxies: JDK Dynamic Proxies used for interfaces and CGLIB Proxies used for classes without interfaces. However, proxy-based AOP has some limitations: Internal method calls bypass the proxy, private methods cannot be intercepted, and Final methods cannot be proxied. For more complex scenarios, AspectJ weaving can be used as an alternative, which allows direct bytecode modification and interception without relying on proxies.
Tuning of Spring Cache
The fine-tuning of the Spring cache system is primarily done through the choice of cache provider. The following table provides an overview of different cache providers, their typical use cases, and the associated advantages and disadvantages.
| CACHE PROVIDER | TYPE | USE CASE | ADVANTAGES | DISADVANTAGES |
|---|---|---|---|---|
| ConcurrentMapCache | Local | Small applications / testing | Simple, no external dependency | No TTL, no eviction |
| Caffeine | Local | High-performance APIs | Very fast, modern eviction algorithms, async loading | Local only |
| EhCache | Local + Persistence | Applications with long-lived cache data | Disk persistence possible | More complex configuration |
| Redis | Distributed | Web apps / microservices | Very fast, cluster-capable | Network latency |
| Hazelcast | Distributed | Cluster applications | In-memory grid, automatic replication | Higher resource consumption |
| Infinispan | Distributed | Enterprise systems | Scalable, supports transactions | Complex operation |
The next table provides an overview of tuning parameters and their effects on the cache system, which can be used in conjunction with a cache provider.
| PARAMETER | MEANING | TYPICAL VALUE / RECOMMENDATION | EFFECT |
|---|---|---|---|
| maximumSize / maxEntries | Maximum number of cache entries | 500–5,000 local, 10k+ distributed | Prevents memory overflow |
| expireAfterWrite / TTL | Time-to-live after write | 5–60 minutes | Prevents stale data |
| expireAfterAccess / TTI | Time-to-idle since last access | 5–60 minutes | Removes rarely used data |
| Eviction Policy | Strategy for removing entries | LRU standard, LFU for hot keys | Optimizes memory usage |
| refreshAfterWrite | Background refresh | Optional | Prevents cache misses |
| initialCapacity | Initial size of the cache | Set high for large caches | Less rehashing / locking |
| backupCount | Replication in cluster | 1–2 | Higher fault tolerance |
| overflowToDisk | Persistence outside of heap | For large data | Reduces heap usage |
Based on the insights gained so far, various strategies can now be applied to achieve improved caching performance. The following table explains some of these strategies.
| Technique | Description | Impact |
|---|---|---|
sync=true |
Prevents cache stampede | Avoids DB storms |
| Local Cache before Redis | Multi-level caching | 10–50× faster responses |
| Increase initialCapacity | Reduces resizing and rehashing | Less lock contention |
| Key optimization | Use simple keys | 10–20% performance improvement |
| Negative caching | Cache null results | Up to 80% DB load reduction |
| Async cache loading | Non-blocking data retrieval | Better scalability |
| Cache warmup | Preload cache at startup | Stable latency after deployment |
Let's break down some of these concepts and analyze a few examples.
Many caches start with a small initial capacity, which causes internal data structures to resize frequently. An example using Caffeine:
Caffeine.newBuilder().initialCapacity(100000).maximumSize(1000000).build();
A larger initial capacity reduces rehash operations and improves scalability.
Another important concept is negative caching, where even negative results are stored in the cache. For example, if a user ID does not exist and every request queries the database again, this can create unnecessary load. By caching null results, this effect can be avoided.
Modern caching libraries like Caffeine also support asynchronous data loading, allowing non-blocking computation and improved parallelism for high-traffic applications.
buildAsync(key -> loadUser(key));
This reduces thread blocking, which is especially beneficial in highly parallelized systems.
After a deployment, many caches are initially empty. This can cause performance spikes if a large number of requests suddenly hit the database. A simple solution is to perform a cache warm-up when the application starts:
@PostConstruct public void warmCache() { service.getUser(1); service.getUser(2); }
This pre-populates the cache with frequently used data at startup.
In addition to the performance strategies mentioned so far, the choice of proxy type for the caching system can also impact performance (proxy-based AOP). In Spring, there are two proxy types: JDK Dynamic Proxies and CGLIB. The following table provides an overview of the differences between them.
| PROXY TYPE | IMPLEMENTATION | PERFORMANCE | TYPICAL USE CASE |
|---|---|---|---|
| JDK Dynamic Proxy | Java Reflection + Interface Proxy | Minimally slower on method calls | Services with interfaces |
| CGLIB Proxy | Bytecode-generated subclass | Minimally faster on calls | Classes without interfaces |
CGLIB generates a subclass of your class and invokes methods directly via bytecode. Internal example:
UserService$$SpringCGLIBProxy extends UserService
Method Call:
proxy.getUser()
↓
interceptor
↓
super.getUser()
No reflection is needed. JDK proxies work via an InvocationHandler. Internally:
Proxy.invoke()
↓
InvocationHandler
↓
Method.invoke()
This uses reflection, which has historically been slower. The following table shows some numerical values illustrating how the choice of proxy type affects overall performance.
| Operation | TIME |
|---|---|
| Regular Call | ~5 ns |
| CGLIB Proxy Call | ~20–40 ns |
| JDK Proxy Call | ~40–80 ns |
When should each proxy type be used? The following table provides an overview.
| Situation | Advise |
|---|---|
| Microservices | CGLIB |
| Spring Boot Standard | CGLIB |
| Interface-heavy Architecture | JDK Proxy |
| Performance | It doesn't matter |
Conclusion
Spring Cache is a powerful mechanism for improving application performance. Caching frequently accessed data significantly reduces the number of expensive operations, such as database queries or external API calls. As a result, applications can achieve lower latency, reduced backend load, and better scalability under high traffic. However, while Spring provides a variety of configuration and tuning options, not all of them have the same impact on performance.
A good example of marginal impact is the selection of the proxy type used by Spring’s AOP infrastructure. Whether the framework uses JDK Dynamic Proxies or CGLIB proxies typically results in only negligible performance differences, since the overhead of proxy invocation is extremely small compared to the cost of database queries, network calls, or complex business logic. Therefore, performance optimization in Spring Cache should focus primarily on architectural and data-access related aspects rather than low-level framework details.
Opinions expressed by DZone contributors are their own.
Comments