skip to content

Contrast @Cacheable, @CachePut, @CacheEvict, and @Caching. When would you use each?

level: middleimportance: must knowfreq 70%

answer

  1. Cacheable = may skip; CachePut = always runs
  2. CacheEvict = remove; allEntries clears all
  3. beforeInvocation → evict even on throw
  4. Caching = container for several ops
  5. CachePut stores RETURN value

basics

~20 s

@Cacheable reads through (skips the method on a hit). @CachePut always runs the method and stores the fresh result (for updates). @CacheEvict removes entries (for deletes/invalidation). @Caching groups several of these annotations on one method.

solid answer

~40 s

The four annotations cover the CRUD lifecycle of a cache entry. @Cacheable is read-through: it may skip the method on a hit. @CachePut ALWAYS executes the method and then writes the return value to the cache — use it on update methods so the cache is refreshed rather than bypassed (never put @Cacheable and @CachePut on the same method with the same key; their behaviors conflict). @CacheEvict removes entries: a single key, or the whole cache when allEntries=true, and can fire beforeInvocation=true so it evicts even if the method throws. @Caching is a container that lets you stack multiple @Cacheable/@CachePut/@CacheEvict on one method — e.g. evict from two caches at once, or evict one key while putting another — which the individual annotations can't express because each is @Repeatable-limited in older styles.

code

java · 20 lines
java
@Service
class BookService {

    @Cacheable(cacheNames = "books", key = "#isbn")
    public Book find(String isbn) { return db.load(isbn); }

    // always runs; refreshes the same entry the reader uses
    @CachePut(cacheNames = "books", key = "#result.isbn")
    public Book update(Book book) { return db.save(book); }

    // evict even if delete throws
    @CacheEvict(cacheNames = "books", key = "#isbn", beforeInvocation = true)
    public void delete(String isbn) { db.remove(isbn); }

    @Caching(evict = {
        @CacheEvict(cacheNames = "books", allEntries = true),
        @CacheEvict(cacheNames = "isbns", allEntries = true)
    })
    public void reloadAll() { db.reload(); }
}

go deeper

for a junior

Knows the one-line purpose of each annotation.

for a middle

Can pick the right annotation per CRUD operation and knows allEntries/beforeInvocation flags.

for a senior

Explains key alignment between reader and writer, return-value semantics of @CachePut, and eviction-on-failure.

for a principal

Discusses cache-coherence strategy across methods, when to prefer eviction over put, and meta-annotation composition for team conventions.

## The lifecycle model Think of a cache entry as having a lifecycle, and each annotation owns one transition: ### `@Cacheable` — read-through (may skip the method) Covered elsewhere: on a hit it returns the stored value without running the body; on a miss it runs and stores. Use on **reads/queries**. ### `@CachePut` — write-through (always runs the method) `@CachePut(cacheNames = "books", key = "#book.isbn")` **always executes the method**, then stores the returned value under the key. It never skips. Use it on **create/update** methods so that after saving, the cache holds the fresh object. The classic mistake is putting `@Cacheable` on an update method — that would skip the DB write on a hit. Corollary: **don't combine `@Cacheable` and `@CachePut` on the same method** with the same key; one wants to skip, the other wants to always run — the result is undefined/contradictory. ### `@CacheEvict` — invalidation (removes entries) `@CacheEvict(cacheNames = "books", key = "#isbn")` removes one entry. Two important flags: - **`allEntries = true`** clears the ENTIRE cache (ignores `key`). Use after bulk operations. - **`beforeInvocation`** — default `false` means eviction happens **after** the method returns successfully, so if the method throws, nothing is evicted. Set `beforeInvocation = true` to evict **before** the method runs, so the entry is removed even if the method later throws. Useful when a stale entry must go regardless of outcome. A `@CacheEvict` method may return `void` — eviction doesn't need a return value. ### `@Caching` — combine multiple ops `@Caching` is a container annotation with `cacheable`, `put`, and `evict` array attributes. Use it when one method must perform several cache operations that a single annotation can't express, for example: - Evict from two different caches. - Evict an old key and put a new one in the same call. - Cache under two different keys with different conditions. ```java @Caching(evict = { @CacheEvict("books"), @CacheEvict(value = "isbns", key = "#book.isbn") }) public void reload(Book book) { ... } ``` ## Gotchas - **`@CachePut` stores the RETURN value, not the argument.** If your save method returns `void` or returns something other than the entity, the cache gets that instead. Return the persisted entity. - **Key alignment matters.** For `@Cacheable(key="#isbn")` on the reader and `@CachePut(key="#book.isbn")` on the writer to cooperate, both must resolve to the *same* key object (equal `hashCode`/`equals`). A mismatch means the put lands under a key no reader ever looks up. - **`allEntries=true` + a `key`** — the `key` is ignored; some read it as a bug. - **`beforeInvocation=false` (default) + exception** → no eviction; a stale entry can survive a failed delete unless you flip the flag. - **Composed annotations:** `@Cacheable`, `@CachePut`, `@CacheEvict` are meta-annotatable, so teams often build custom `@BookCache` shortcuts. ## When to use which - Read/lookup → `@Cacheable`. - Create/update that should refresh the cache → `@CachePut`. - Delete or explicit invalidation → `@CacheEvict` (add `beforeInvocation=true` when the entry must go even on failure; `allEntries=true` for bulk). - Multiple operations in one method → `@Caching`.

  • Why is putting @Cacheable on an update/save method a bug?
    @Cacheable may return a cached value and skip the method body. On a save method that means the database write is skipped whenever the key is already cached. Update methods should use @CachePut, which always executes and then refreshes the entry.
  • What does beforeInvocation=true change on @CacheEvict?
    It evicts the entry BEFORE the method runs, so the removal happens even if the method throws an exception. The default (false) evicts only after a successful return, leaving stale entries behind on failure.

saying these in an interview costs you the question

  • Thinking @CachePut skips the method like @Cacheable does
  • Believing @CacheEvict with allEntries=true still honors the key attribute
  • Assuming @CachePut caches the method argument rather than the return value
  • Putting @Cacheable and @CachePut on the same method expecting them to combine cleanly

context