ReviseAlgo Logo

Caching with Redis

Spring Cache Abstraction — @Cacheable, @CacheEvict, @CachePut

Enabling caching mechanisms, setting cache keys, and evicting entries.

Last Updated: June 15, 2026 15 min read

Introduction

Writing boilerplate code to connect to caches, check keys, verify expirations, and evict stale records clutter business logic. The Spring Framework provides a declarative **caching abstraction** that allows developers to add caching capabilities to methods using simple annotations.

This framework abstraction is provider-agnostic, meaning you can configure it to use in-memory JVM maps, Caffeine, Ehcache, or external Redis backend servers without altering your Java service code.

Why It Matters

Declarative caching via annotations separates database access optimizations from functional requirements. By removing boilerplate code from services, code readability increases and testing is simplified. It also provides built-in support for thread synchronization and key generation logic.

Real-World Analogy

Think of a lazy assistant working for an accountant. Instead of the accountant manually looking up receipts in drawers and writing copies in a personal notepad (boilerplate code), they tell the assistant: "When I ask for a tax report for client X, look it up. Write a copy in your notebook. Next time I ask for client X, hand me your notebook copy instead of searching the drawers."

How It Works

Spring's caching abstraction provides three core annotations to control caching lifecycles:

  • @Cacheable: Placed on read methods. Spring checks the cache using the generated key. If found, method execution is skipped, and cached value is returned. If not found, method runs, and result is cached.
  • @CachePut: Placed on write/update methods. The method is always executed, and its return value is written to the cache, updating existing keys.
  • @CacheEvict: Placed on delete/update methods. Removes entries from the cache to prevent stale reads. Supports evicting a single key or clearing all entries (allEntries = true).

Dynamic cache keys and evaluation conditions are written using Spring Expression Language (SpEL), such as referencing method parameters: key = "#productId".

Internal Architecture

Spring caching relies on **Aspect-Oriented Programming (AOP)** proxies. When you enable caching using @EnableCaching:

  • Spring wraps annotated beans inside dynamic runtime proxies.
  • When an external bean invokes the annotated method, the request goes to the proxy, not the actual method.
  • The proxy consults the configured CacheManager (which manages cache names and backends).
  • The proxy handles cache check/lookup and serialization behind the scenes, then returns the data.

Visual Explanation

Loading diagram…

Practical Example

Below is a complete Spring Boot service class demonstrating declarative caching with custom keys and evictions:

Common Mistakes

  • Self-Invocation (Internal Calls): Calling a @Cacheable method from another method inside the same class bypasses the Spring AOP proxy, executing the database call directly and rendering caching inactive.
  • Mutable Objects in Local Cache: Returning a cached object and directly modifying its fields. This modifies the cache object by reference, contaminating the cache for other threads.
  • Complex Non-Serializable Objects: Caching objects that cannot be serialized when using an external cache provider like Redis, throwing runtime SerializationExceptions.

Quick Quiz

Q1: Why does calling a @Cacheable method internally (e.g. this.myCachedMethod()) from within the same class fail to retrieve data from the cache?

A) Because internal methods are run in a separate execution thread.

B) Because the call bypasses the Spring AOP Proxy wrapper.

C) Because SpEL parameters only bind to public static resources.

D) Because the local JVM garbage collector flushes the entries.

Answer: B — Spring caching relies on AOP proxies. When a method invokes another method on 'this', the proxy is bypassed, and annotations are not processed.

Q2: Which Spring cache annotation is appropriate for an update operation where you always want to run the database write and update the cache copy?

A) @Cacheable

B) @CacheEvict

C) @CachePut

D) @CacheResolver

Answer: C — @CachePut executes the annotated method every time and updates the target cache key with the returned object, keeping the cache fresh.

Scenario-Based Challenge

Production Scenario:

You have a service class that manages report generation. You annotated a method with @Cacheable(value = "reports", key = "#reportId"). You notice that calling this method from a controller caches correctly. However, you also have a scheduled cronjob in the same service class that calls this method weekly to pre-warm the cache, but it always queries the database directly. Why is this, and how do you resolve it?

View Solution

The scheduled cronjob is calling the cached method internally (self-invocation), which bypasses the Spring AOP Proxy.

Solution:
1. **Self-Injection (Lazy Autowire):** Inject the service instance into itself and call the method via the injected bean:


2. Alternatively, move the scheduled cronjob to a separate class (like ReportScheduler) and autowire ReportService there to ensure all invocations pass through the Spring proxy.

Debugging Exercise

Broken Configuration:

The developer enabled caching in Spring Boot, but they are receiving a runtime exception: IllegalArgumentException: Cannot find cache named 'products'. Below is the configuration class and service method:

View Debugging Steps & Fix

Reason:
1. The developer forgot to declare the @EnableCaching annotation in their Spring Boot configuration class. Without it, Spring does not create caching proxies.
2. If a custom cache manager is registered (like a Redis Cache Manager) without defining default cache name fallbacks, attempts to reference non-existent cache names will throw runtime exceptions if the cache manager is not configured to create dynamic caches.

The Fix: Add @EnableCaching on the main application class or a configuration class, and configure dynamic cache creation:

Interview Questions

1. Conceptual: How does Spring AOP enable caching? Explain the proxy pattern's role.

Spring uses Aspect-Oriented Programming (AOP) to intercept class executions. At application startup, classes containing cache annotations are wrapped in dynamic proxy objects. When an external object invokes a method on the proxy, the proxy intercepts the call. It checks if the cache holds a value matching the method's arguments. If present, it bypasses the real method execution and returns the value. If not, it executes the target method and caches the result.

2. Technical: What is SpEL (Spring Expression Language), and how can you use it to conditionally cache a method's return value only if the argument meets a specific criteria?

SpEL is an expression language that supports querying and manipulating an object graph at runtime. In Spring Caching, it allows referencing method inputs and outcomes dynamically. You can use the condition and unless parameters on @Cacheable to write logical rules:
- condition (before execution): @Cacheable(value="users", condition="#userId > 1000") only caches user lookups if the argument is greater than 1000.
- unless (after execution): @Cacheable(value="users", unless="#result == null") will execute the method but will not store the result if the returned user object is null.

Production Considerations

Avoid caching null or empty database states unless explicit unless = "#result == null" properties are configured, preventing empty lookups from polluting memory. Monitor proxy startup limits (JDK dynamic proxies require interfaces, whereas CGLIB subclassing supports concrete classes).