skip to content

What does @EnableCaching do, and how does @Cacheable turn a method into a cached method?

level: juniorimportance: must knowfreq 78%

answer

  1. @EnableCaching = turn on the proxy
  2. hit → skip method; miss → run + store
  3. default key = SimpleKeyGenerator(args)
  4. self-invocation bypasses proxy
  5. null is cached too

basics

~20 s

@EnableCaching switches on Spring's caching support (an AOP proxy). @Cacheable on a method means: before running it, look in the named cache by key; if found, return the cached value and skip the method; if not, run it and store the result.

solid answer

~40 s

@EnableCaching (on a @Configuration class) registers the infrastructure that wraps caching-annotated beans in an AOP proxy — nothing is cached without it. @Cacheable("books") marks a method as read-through: on each call the proxy computes a key (default: the method arguments via SimpleKeyGenerator), looks it up in the named Cache obtained from the CacheManager, and on a hit returns the stored value WITHOUT invoking the method. On a miss it invokes the method, stores the return value under that key, then returns it. Because it works through a proxy, only external calls into the bean are intercepted — a self-invocation (this.method()) bypasses caching. The value is cached whether or not you expected it (e.g. null is cached unless excluded), so caching applies to any method whose result is a pure function of its inputs.

code

java · 12 lines
java
@Configuration
@EnableCaching
class CacheConfig { }

@Service
class BookService {
    // key defaults to the isbn argument (SimpleKeyGenerator)
    @Cacheable("books")
    public Book findByIsbn(String isbn) {
        return slowLookupFromDb(isbn); // skipped on a cache hit
    }
}

go deeper

for a junior

Must know @EnableCaching turns it on and @Cacheable does 'check first, run on miss, store the result'.

for a middle

Should explain the proxy mechanism, default SimpleKeyGenerator, and the self-invocation caveat.

for a senior

Explains that null is cached, hit skips the body, multi-cache lookup order, and public/proxy constraints.

for a principal

Frames it as cache-aside via AOP, discusses proxy vs AspectJ mode, thundering-herd on concurrent misses, and side-effect safety.

## The two pieces Spring's cache abstraction is declarative caching layered on Spring AOP. There are two mandatory ingredients: 1. **`@EnableCaching`** — placed on a `@Configuration` class. It imports the caching infrastructure: a `BeanPostProcessor` that creates AOP proxies around any bean containing cache annotations, plus the `CacheInterceptor`/`CacheAspectSupport` that actually runs the logic. **Without `@EnableCaching`, all cache annotations are silently ignored** — the beans are just plain objects. (Spring Boot auto-configures this for you when `spring-boot-starter-cache` is on the classpath and a `CacheManager` exists, but the annotation is still what turns it on.) 2. **A `CacheManager` bean** — the registry that hands out named `Cache` instances. With no explicit provider, Boot falls back to a `ConcurrentMapCacheManager` (in-memory `ConcurrentHashMap`). (Which concrete manager to pick is a separate topic.) ## `@Cacheable` semantics — "read-through" `@Cacheable(cacheNames = "books")` (or the shorthand `@Cacheable("books")`) marks a method for cache-aside/read-through behavior. On every intercepted call the proxy: 1. **Computes a key** — by default from the method arguments using `SimpleKeyGenerator`: no args → `SimpleKey.EMPTY`; one arg → that arg itself; multiple args → a `SimpleKey` wrapping them all (its `equals`/`hashCode` combine every argument). 2. **Looks the key up** in the `Cache` named `books` (fetched from the `CacheManager`). 3. **Cache hit** → returns the stored value and **does not execute the method body**. This is the whole point: the expensive work is skipped. 4. **Cache miss** → executes the method, **stores** the returned value under the key via `Cache.put`, then returns it. You can name several caches: `@Cacheable({"books", "isbns"})` checks each in order; the first hit wins, and on a miss the result is written to all of them. ## Critical gotchas - **Proxy-based → self-invocation is not intercepted.** If method `a()` calls `this.b()` where `b()` is `@Cacheable`, the cache is bypassed because the call doesn't cross the proxy boundary. Split into another bean or inject a self-reference to fix it. - **`null` and every return value are cached** unless you exclude them with `unless` or a `condition`. A cached `null` still counts as a hit next time. - **Not a distributed lock.** Two concurrent misses can both run the method before either stores its result (the `sync` flag addresses this — separate question). - **`@Cacheable` is for reads.** Because it may skip the method, never use it on a method whose side effects must always run — use `@CachePut` for that. - **Public methods only** with the default (proxy) mode; `private`/`final` methods and `final` classes can't be proxied by CGLIB. ## When to use Wrap methods whose result is a deterministic function of their arguments and are expensive relative to a map lookup: repository/`findById`, remote API calls, heavy computations. Do not cache methods with important side effects or highly volatile inputs.

  • Why does calling a @Cacheable method from another method in the same class sometimes not cache?
    Caching is applied by an AOP proxy that only intercepts calls arriving from outside the bean. A `this.method()` self-invocation never crosses the proxy, so the interceptor never runs. Fix by moving the method to another bean or injecting a self-reference.
  • What is used as the cache key when the method has no arguments?
    `SimpleKeyGenerator` returns `SimpleKey.EMPTY`, a shared constant key — so a no-arg @Cacheable method effectively caches a single value in that cache.

saying these in an interview costs you the question

  • Thinking cache annotations work without @EnableCaching (or a starter that enables it)
  • Believing self-invocation (this.method()) is still cached
  • Assuming null results are never cached
  • Confusing @Cacheable with @CachePut (thinking @Cacheable always runs the method)

context