An Introduction to @Cacheable, @CacheEvict, and Other Cache-related Annotations in Spring

Alexander Obregon
11 min readAug 24, 2023

--

Image Source

Introduction

Caching is an optimization technique that stores frequently used data or computations to make subsequent access faster. In the world of software, caching reduces the number of time-consuming operations, like database calls or complex computations. Spring Framework, one of the most widely used Java frameworks, provides a rich set of cache-related annotations that make the caching mechanism more efficient and straightforward. In this article, we will go into some of the primary cache-related annotations: @Cacheable, @CacheEvict, and more.

Why Use Caching in Spring?

Caching, at its core, is a method of storing frequently accessed data in a manner that allows for faster retrieval. In the context of software applications, it becomes indispensable due to the unique challenges posed by modern data-driven environments. Here’s a deeper dive into why caching is essential, especially in Spring applications:

  1. Reduced Latency: At the forefront of the benefits is the drastic reduction in data retrieval times. Databases, especially when under heavy load or when querying complex data structures, can introduce significant latency into the system. Caching that data means subsequent requests retrieve it almost instantaneously, leading to a smoother user experience.
  2. Decreased Database Load: Every query made to a database consumes resources. It’s not just about data retrieval time; it’s also about CPU load, memory usage, and I/O operations. By serving data from the cache, applications can significantly reduce the number of actual database hits, preserving system resources and potentially reducing costs, especially in cloud environments where you pay for what you use.
  3. Enhanced Reliability: Databases can fail, or sometimes they may require maintenance. When cached data satisfies user requests, applications can continue functioning gracefully, albeit with limited capabilities, even during database downtimes.
  4. Cost Efficiency: Frequent database calls, especially in distributed architectures like microservices, can lead to increased network costs. Caching helps in reducing these calls, thereby minimizing network-related costs.
  5. Flexible Scalability: As applications grow, so do the demands on the database. Caching allows applications to scale more gracefully. By offloading a chunk of the data retrieval operations to cache, you can ensure that your database is not overwhelmed with requests, buying you time to strategize and implement long-term scaling solutions.
  6. Improved User Experience: From a user’s perspective, performance is a significant aspect of user experience. A responsive application retains users more effectively than one that keeps them waiting. By using caching effectively, Spring applications can offer a faster, more responsive experience to end users.
  7. Strategic Data Availability: Caching isn’t just about speed; it’s also about strategy. With caching mechanisms, you can prioritize which data is cached. This means high-priority data, like frequently viewed items or high-demand configurations, can always be readily available, ensuring optimal application performance where it matters most.
  8. Integration with Spring Ecosystem: Spring offers seamless integration with a variety of caching solutions, from in-memory databases like Redis and Ehcache to distributed caches like Hazelcast. This makes it easy for developers to implement and configure caching tailored to their application’s needs.

Setting Up a Cache Manager

A cache manager is the backbone of the caching mechanism in a Spring application. It’s responsible for managing cached data, ensuring efficient storage, retrieval, and eviction of entries. Without it, the annotations like @Cacheable or @CacheEvict wouldn't know where to store or retrieve data. Let’s explore its significance, configuration, and the various choices Spring offers:

Significance of a Cache Manager

  • Centralized Control: It provides a centralized point of control for all caching operations, ensuring consistency.
  • Lifecycle Management: The cache manager oversees the life cycle of cache regions, from creation to destruction.
  • Resource Allocation: Decides how much memory or disk space should be allocated to the cache and handles situations when these limits are reached.

Basic Configuration with ConcurrentMapCacheManager

Spring offers a simple in-memory cache manager called ConcurrentMapCacheManager. It's based on Java's ConcurrentHashMap, which means it's thread-safe and suitable for most single-instance applications.

@Configuration
@EnableCaching
public class CacheConfig {

@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("books", "authors");
}
}

The @EnableCaching annotation signals Spring's caching mechanism to look for other cache-related annotations throughout the application.

Diverse Cache Manager Options

While ConcurrentMapCacheManager is excellent for simple use cases, Spring offers integrations with several other cache solutions for diverse needs:

  • EhCache: A strong, scalable, and configurable cache solution.
  • Redis: A popular in-memory data structure store, used as a cache or a datastore.
  • Hazelcast: A distributed in-memory data grid, which can be used for caching in microservices architectures.
  • Caffeine: A high-performance Java-based caching library.

Depending on the use case, you might opt for one over another. For example, distributed applications might benefit more from Hazelcast or Redis, while a single-instance application might find EhCache or Caffeine more suitable.

Custom Cache Manager

For unique use cases, Spring allows developers to create custom cache managers by implementing the CacheManager interface. This provides flexibility to tailor caching behavior exactly to an application's needs.

Tuning and Configuration

Beyond just setting up, most cache managers come with a host of configuration options. These might include settings related to:

  • Eviction Policies: Deciding when to remove items, based on policies like LRU (Least Recently Used).
  • Size Limits: Constraining the cache to a particular size.
  • TTL (Time-To-Live): Setting a time after which cached items are automatically removed.
  • Persistence: Deciding if cached data should be written to disk.

Monitoring and Maintenance

Many cache managers come with built-in tools or integrations that allow monitoring the cache’s performance. This is invaluable in ensuring the cache serves its purpose of enhancing performance without becoming a bottleneck.

@Cacheable

The @Cacheable annotation tells Spring to store the method's return value in the specified cache. Subsequent calls to this method with the same parameters will fetch the result from the cache instead of executing the method.

Usage:

@Service
public class BookService {

@Cacheable("books")
public Book findBookById(Long id) {
// Simulating a time-consuming method
return databaseCallToFindBookById(id);
}
}

Every time you call findBookById with the same id, Spring will return the cached result instead of executing the databaseCallToFindBookById method.

Parameters:

  • value or cacheNames: The names of the caches where the results are stored.
  • key: SpEL expression for computing the key dynamically.
  • condition: SpEL expression that determines if the method should be cached.

@CacheEvict

While caching is beneficial, there might be situations where cached data needs to be evicted (removed). The @CacheEvict annotation is used for this purpose. It ensures that under specific operations, the cache remains up to date.

Usage:

@Service
public class BookService {

@CacheEvict(value = "books", key = "#id")
public void deleteBookById(Long id) {
// Method to delete book from the database
actualDatabaseDeleteMethod(id);
}
}

When deleteBookById is called, the corresponding entry in the "books" cache will be evicted.

Parameters:

  • allEntries: If set to true, it will clear all entries from the specified cache.
  • beforeInvocation: If set to true, it will clear the cache before the method is executed.

Combining Annotations

Spring’s caching annotations aren’t just individual tools; they’re building blocks that can be layered and combined to craft sophisticated caching strategies tailored to specific application requirements. This ability to combine annotations brings forth a realm of possibilities. Let’s delve into the details:

Why Combine Annotations?

  • Richer Behavior: Single annotations are designed for specific tasks. Combining them allows for more complex operations, reflecting intricate business logic.
  • Code Clarity: Using multiple annotations on a single method can provide a clear overview of the caching strategy being employed, making it more understandable for developers revisiting the code.
  • Dynamic Cache Management: By pairing annotations, you can create dynamic cache behaviors responding to different conditions or triggers.

Example:

@Cacheable("books")
@CacheEvict(value = "books", allEntries=true)
public Book complicatedBookOperation(Long id) {
// Some complicated logic
}

In the above method, @Cacheable ensures that if the result of complicatedBookOperation for a specific id is already in the cache, it's returned without executing the method. Meanwhile, @CacheEvict ensures that after executing the method, all entries in the "books" cache are evicted. This could be useful in scenarios where this method affects data that might render the entire cache stale.

Scenarios for Combining Annotations

  • Refresh and Evict: Consider scenarios where data is frequently updated but also accessed regularly. Here, combining @CachePut (to update the cache) with @CacheEvict can ensure the cache is always current, and old irrelevant entries are evicted.
  • Conditional Caching: By using @Cacheable with conditions and combining it with @CacheEvict, you can cache certain results while ensuring the cache remains up-to-date when specific updates occur.
  • Bulk Operations: In applications where bulk operations on data are common, combining multiple caching strategies ensures that the cache reflects these bulk changes without becoming a bottleneck or serving stale data.

Things to Watch Out For

  • Order of Execution: When combining annotations, it’s essential to understand the order in which they’re executed to predict the resultant cache behavior accurately.
  • Performance Implications: While combining annotations can optimize caching, if not done judiciously, it can also introduce performance overheads. Always test and monitor the application’s performance when introducing new caching combinations.
  • Readability: While combining annotations offers power, overdoing it can make the code less readable. Strive for a balance between optimization and clarity.

Extensibility with @Caching

For more advanced scenarios, where you might find yourself combining multiple @Cacheable, @CacheEvict, or @CachePut operations, Spring provides the @Caching annotation. This allows for cleaner and more organized configuration when multiple cache operations are necessary for a single method.

Additional Cache-related Annotations

Spring’s caching arsenal isn’t limited to the commonly known annotations like @Cacheable and @CacheEvict. There's a suite of lesser-known annotations and attributes that can refine caching strategies and provide granular control over cache behavior. Here's a comprehensive exploration:

@CacheConfig

Purpose:

  • This class-level annotation is used to share some common cache-related settings across all methods in a class.

Benefits:

  • Consistency: Ensures that all caching methods within a class adhere to a consistent configuration.
  • Reduced Redundancy: Eliminates the need to specify common attributes with each method, keeping code DRY (Don’t Repeat Yourself).

Usage:

@CacheConfig(cacheNames = "items")
public class ItemService {
@Cacheable
public Item findItem(Long id) {
// Logic here
}
}

Here, the @Cacheable method within ItemService will use the cache named "items" without explicitly specifying it.

@CachePut

Purpose:

  • Ensures that a method’s return value is always placed in the cache, effectively updating the cache every time the method is executed.

Benefits:

  • Dynamic Updates: Ideal for methods that update resources, ensuring the cache is always fresh.
  • Versatility: Can be combined with @CacheEvict or @Cacheable to provide a broader range of cache behaviors.

Usage:

@CachePut(cacheNames = "items", key = "#item.id")
public Item updateItem(Item item) {
// Update and return the item
}

@CacheConfig’s cacheResolver

Purpose:

  • Instead of defining cache names directly, this attribute allows you to define a custom CacheResolver bean that dynamically resolves the cache at runtime.

Benefits:

  • Dynamic Cache Resolution: Useful for scenarios where cache destinations need to be determined dynamically.
  • Custom Logic Integration: Provides an opportunity to incorporate custom logic in choosing the appropriate cache.

Usage:

@CacheConfig(cacheResolver = "myCacheResolver")
public class DynamicCacheService {
// Methods here
}

The myCacheResolver bean should implement the CacheResolver interface and define the logic to resolve the cache.

Key Generation

Purpose:

  • While the key attribute in many cache-related annotations is handy, sometimes you need a more dynamic way to generate keys. Spring allows for custom key generation through the keyGenerator attribute.

Benefits:

  • Complex Key Structures: Ideal for situations where cache keys need to be constructed using multiple method arguments or other dynamic data.
  • Consistency Across Methods: A custom key generator ensures a consistent key generation strategy across multiple cached methods.

Usage:

@Cacheable(cacheNames = "orders", keyGenerator = "myKeyGenerator")
public Order getOrder(Long userId, Date date) {
// Logic here
}

The myKeyGenerator bean should implement the KeyGenerator interface and provide the custom key generation logic.

Custom Cache Condition

Purpose:

  • Annotations like @Cacheable or @CachePut support the condition attribute, enabling cache operations only when certain conditions are met.

Benefits:

  • Fine-grained Control: Offers granular control over when to cache, catering to intricate business requirements.

Usage:

@Cacheable(cacheNames = "premiumUsers", condition = "#user.isPremium()")
public UserProfile getPremiumUserProfile(User user) {
// Logic here
}

In this example, caching will occur only if the provided user is a premium user.

Best Practices

Incorporating caching into your Spring application can significantly boost performance. However, to maximize its benefits and avoid pitfalls, adhering to best practices is paramount. Here’s a deep dive into the do’s and don’ts:

Understanding the Business Needs

  • Relevance: Ensure that caching is relevant to the problem at hand. Not all methods or data benefit from caching. Analyze if the data retrieval is time-consuming or if the data remains static for a considerable duration.
  • Freshness: For data that changes frequently, consider the trade-off between performance and data freshness. Is it acceptable for users to see slightly outdated data, or is real-time data essential?

Optimal Cache Configuration

  • Size Matters: Allocate appropriate memory or storage size for the cache. An undersized cache may lead to frequent evictions, while an oversized cache might waste resources.
  • Eviction Policies: Choose an eviction policy (e.g., LRU, LFU) that aligns with your application’s access patterns. This ensures that the cache remains effective over time.

Use the Right Cache Type

  • In-memory vs. Distributed: While in-memory caches like ConcurrentMapCacheManager or Caffeine are quick, distributed caches like Redis or Hazelcast provide scalability across multiple instances or microservices.
  • Persistence: If resilience against failures is a priority, opt for cache solutions that offer data persistence, ensuring that cached data survives application restarts.

Be Mindful of Cache Granularity

  • Granularity Choices: Decide whether to cache entire objects, subsets of objects, or just raw data. Each choice impacts storage utilization and the complexity of invalidating cache entries.
  • Too Granular: Caching at a very granular level, like individual fields, can lead to overhead in cache management and may negate the benefits.
  • Too Coarse: Large objects may occupy significant space and could lead to cache evictions, especially if only a small portion of the object is frequently accessed.

Dynamic Cache Management

  • Use Conditional Caching: With the condition attribute in cache annotations, you can introduce logic to cache selectively.
  • Time-To-Live (TTL): Setting a TTL ensures data doesn’t remain in cache indefinitely. Adjust the TTL based on how often the underlying data changes.

Key Design Considerations

  • Consistency: Ensure that keys are generated consistently. Inconsistent keys can lead to cache misses or multiple cache entries for the same data.
  • Complexity: While keys should be unique, avoid making them overly complex. Overhead in key generation can reduce the benefits of caching.

Handle Cache Failures Gracefully

  • Fallback Strategies: Implement mechanisms to handle cache failures, like serving data directly from the source or using stale cache data.
  • Monitoring & Alerts: Regularly monitor cache hit and miss ratios, latency, and eviction rates. Set up alerts for anomalies to address issues promptly.

Regularly Test & Evaluate

  • Performance Testing: Regularly test the performance of cached vs. non-cached operations. This helps in identifying bottlenecks and ensures the cache adds value.
  • Eviction Testing: Simulate scenarios where the cache reaches its maximum size. This helps in understanding eviction behaviors and tuning cache sizes.

Documentation and Comments

  • Clear Comments: Whenever you implement caching on a method or service, leave clear comments explaining why caching was used and what the expected behaviors are.
  • Documentation: Maintain comprehensive documentation of the caching strategy, including cache names, TTL values, and any custom logic used.

Stay Updated

  • Framework Updates: As Spring evolves, so do its caching capabilities. Regularly check for updates or improvements that can be incorporated into your application.
  • Cache Solution Updates: External caching solutions like Redis or EhCache also receive updates and improvements. Ensure you're utilizing their full potential.

Conclusion

Spring’s cache-related annotations provide a powerful and flexible mechanism to optimize applications. By understanding and effectively using @Cacheable, @CacheEvict, and other annotations, developers can significantly improve the responsiveness and scalability of their Spring applications. As always, while caching brings numerous benefits, it's crucial to use it judiciously and monitor its impact on the application.

  1. Spring Framework Caching
  2. Caching Data with Spring
  3. EhCache Documentation
  4. Redis Documentation
  5. Baeldung’s Guide to Spring Cache
Spring Boot icon by Icons8

--

--

Alexander Obregon

Software Engineer, fervent coder & writer. Devoted to learning & assisting others. Connect on LinkedIn: https://www.linkedin.com/in/alexander-obregon-97849b229/