Caching Issues With the Spring Expression Language
Spring Expression Language is a flexible way to evaluate expressions at runtime. However, in the context of caching, this flexibility can lead to errors.
Join the DZone community and get the full member experience.
Join For FreeLet's imagine a web application where, for each request, it must read configuration data from a database. That data doesn't change usually, but the application, in each request, must connect, execute the correct instructions to read the data, pick it up from the network, etc. Imagine also that the database is very busy or the connection is slow. What would happen? We would have a slow application because it is reading continuously data that hardly changes. A solution to that problem could be using a cache within the Spring framework.
Spring caching is based on a simple principle:
- A cache key is calculated
- If an entry for this key exists → return from the cache
- If no entry exists → the method is executed and the result is stored
The cache key is crucial, and this is where SpEL plays a central role.
The Spring Expression Language (SpEL) is a powerful and flexible tool within the Spring Framework that enables the evaluation and manipulation of expressions at runtime. It provides a dynamic way to access objects and their properties, perform calculations, and even implement complex logical operations — all at runtime. SpEL can be used with both XML-based and annotation-based Spring configurations, simplifying development by reducing boilerplate code while enhancing flexibility. With its ability to assign values dynamically at runtime, SpEL offers an elegant solution to many common software development challenges and contributes to increased efficiency. For more information, check out the DZone article on learning the Spring Expression Language. SpEL is an expression language that allows you to:
Access method parameters (#id), Read object properties (#user.name), Invoke methods (#list.size()), Use static classes (T(java.lang.Math).PI), Formulate logical and conditional expressions.
In caching, SpEL is primarily used for calculating cache keys.
@Cacheable(value = "users", key = "#id")
public User findUser(Long id) {
...
}
Different Caching Issues When Using SpEL
1. Unstable Cache Keys
A common mistake is using non-deterministic expressions.
@Cacheable(value = "orders", key = "T(java.util.UUID).randomUUID()")
Problem: The key is different with every call, meaning the cache is never hit and caching is not implemented. The better way is:
@Cacheable(value = "orders", key = "#orderId")
2. Use of Mutable Object States
When SpEL accesses complex or mutable objects, the same method call can generate different cache keys.
@Cacheable(value = "products", key = "#product")
Problem: equals() / hashCode() may not be stable, and the object's state changes after caching, leading to multiple cache entries for the 'same' data. The solution is to cache based on an object's field:
@Cacheable(value = "products", key = "#product.id")
3. Different Keys in @Cacheable and @CacheEvict
A very dangerous mistake occurs when different SpEL expressions are used for reading and evicting.
@Cacheable(value = "users", key = "#user.id")
public User getUser(User user) { ... }
@CacheEvict(value = "users", key = "#user.username")
public void updateUser(User user) { ... }
Problem: The cache entry is never invalidated, and outdated data remains in the cache. A possible solution would be to always use the identical key strategy, ideally a central ID.
4. Null Values and Exceptions
SpEL expressions can fail when values are null.
@Cacheable(value = "users", key = "#user.address.city")
Problem: NullPointerException Due to a missing address, the cache mechanism failed. To avoid such issues, you could, for example, add additional conditions:
@Cacheable(value = "users", condition = "#user != null")
5. Performance Issues Due to Complex SpEL Expressions
SpEL is evaluated on every method call. Complex expressions can incur measurable performance costs.
@Cacheable(
value = "data",
key = "#a.id + '-' + #b.name + '-' + T(java.time.Instant).now()"
)
Problem: Expensive evaluation, the key changes constantly, resulting in poor performance and no cache hits.
Recommendation: Keep SpEL simple, avoid method calls with side effects, and refrain from using time-based or random values.
Caching With Redis and Caffeine: A Practical Guide
1. Caching With Redis
Redis is a distributed in-memory data store that is ideal for caching. It is often used in scalable, distributed applications where caching needs to be shared across multiple instances.
First, we need to add the Redis and Spring Cache dependencies to the pom.xml (for Maven):
<dependencies>
<!-- Spring Cache und Redis -->
<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>
</dependencies>
Next, we add the Redis connection settings in application.properties:
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password= # falls Passwort gesetzt ist
In the Spring configuration, we need to configure the CacheManager and the RedisConnectionFactory.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues();
}
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(cacheConfiguration())
.build();
}
}
Now we can use the @Cacheable annotation to enable the caching mechanism:
@Service
public class UserService {
@Cacheable(value = "users", key = "#userId")
public User getUserById(Long userId) {
simulateSlowService();
return new User(userId, "John Doe");
}
private void simulateSlowService() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
@Cacheable: This annotation ensures that the method's return value is stored in the cache. When the same method is called with the same parameters, the value is returned directly from the cache instead of executing the method again.
value specifies the name of the cache, and the key parameter is used to compute the cache key.
When you call the getUserById method with a specific userId, the value is stored in the Redis cache. On the next call to the same method with the same userId, the value is returned directly from the cache, significantly improving performance.
2. Caching With Caffeine
Caffeine is an in-memory cache that is faster and easier to configure than Redis, as it doesn't rely on network communication and requires no additional infrastructure. Add Caffeine as a dependency in the pom.xml:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.0.0</version>
</dependency>
Create a configuration class for caching with Caffeine:
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000));
return cacheManager;
}
}
The usage of the @Cacheable annotation is the same as in the Redis example, but now Caffeine is used to store the cache in memory:
@Service
public class ProductService {
@Cacheable(value = "products", key = "#productId")
public Product getProductById(Long productId) {
simulateSlowService();
return new Product(productId, "Product A");
}
private void simulateSlowService() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
When the getProductById method is called, and the return value is stored in the Caffeine cache. If you call the method again with the same parameters, the result is retrieved immediately from the cache.
Conclusion
Spring Expression Language is a powerful tool, but when used with caching, it can quickly lead to hard-to-find bugs. The most common causes are unstable cache keys, inconsistent SpEL expressions, and overly complex or dynamic calculations.
Those who use SpEL in caching consciously and sparingly can benefit from high flexibility without performance or consistency issues. Therefore, use primitive or simple keys (ID, String, Long), ensure consistent cache keys across @Cacheable, @CachePut, and @CacheEvict, and avoid random values, timestamps, and complex object graphs. SpEL expressions should be short, stable, and deterministic, and the cache behavior should be explicitly tested (hits & misses). For clean caching with SpEL, it is best to use an established framework like Redis or Caffeine. Depending on the use case, these frameworks have their own advantages and disadvantages.
| FEATURE |
REDIS |
CAFFEINE |
|---|---|---|
| Usage |
Distributed cache (over networks) |
Local cache in memory |
| Performance |
Slow latency due to network access |
Very fast, as it's in memory |
| Scalability |
Very good for distributed systems |
For local caches on a single instance |
| Storage Size |
Very large, can store massive data | Limited size based on memory |
| Infrastructure |
Requires Redis server (external infrastructure) | No additional infrastructure needed |
| Complexity |
Slightly more complex to manage |
Simple and lightweight |
Redis is ideal for distributed systems and applications that need to scale, especially when many instances are involved, and data must be shared across multiple servers. Caffeine is perfect for local caches where performance is crucial, and the application does not require a distributed infrastructure.
Both caching solutions integrate seamlessly with Spring and provide an easy way to improve your application's performance through SpEL-based caching!
Opinions expressed by DZone contributors are their own.
Comments