Contrast @Cacheable, @CachePut, @CacheEvict, and @Caching. When would you use each?
answer
- Cacheable = may skip; CachePut = always runs
- CacheEvict = remove; allEntries clears all
- beforeInvocation → evict even on throw
- Caching = container for several ops
- 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 sThe 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@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
Knows the one-line purpose of each annotation.
Can pick the right annotation per CRUD operation and knows allEntries/beforeInvocation flags.
Explains key alignment between reader and writer, return-value semantics of @CachePut, and eviction-on-failure.
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