@Cacheable is one annotation with a lot of invisible behaviour: when exactly does it skip your method? what does @CachePut do differently? why did my value go stale — or never expire? So I built a visualizer that shows every hit, miss, store, and eviction on a live cache.
▶ Live demo: https://dev48v.github.io/cache-visualizer/
Source (single file, zero deps): https://github.com/dev48v/cache-visualizer
The whole point of caching, in one number
Click getUser(1) → cache MISS: the DB query runs (~800ms) and the result is stored. Click it again → cache HIT: instant, and your method never runs. The "DB calls saved" counter climbing is exactly why you cache.
That "method never runs" bit matters more than people think — any side effect inside a @Cacheable method is skipped on a hit. Cache pure reads, not things with side effects.
The three annotations aren't the same
@Cacheable("users") // runs the method ONLY on a miss, then caches
public User getUser(Long id) { ... }
@CachePut(value="users", key="#u.id") // ALWAYS runs, then refreshes the entry
public User updateUser(User u) { ... }
@CacheEvict(value="users", key="#id") // removes the entry (or allEntries=true)
public void deleteUser(Long id) { ... }
The demo makes the difference concrete: @Cacheable short-circuits on a hit; @CachePut always executes and bumps the cached value's revision; @CacheEvict drops it so the next read misses again.
Cache-aside + TTL, animated
Each call runs the cache-aside flow — Caller → @Cacheable proxy → check cache → (hit) return, or (miss) → Database → store → return — which is exactly what Redis or Caffeine do behind the annotation. Turn on TTL and each entry gets a countdown bar; after it expires, the next call reloads it. That's the answer to both "why is my cache stale?" and "why does it never update?"
The gotcha that bites everyone
Self-invocation bypasses the cache. Calling a @Cacheable method from within the same bean (this.getUser(id)) skips Spring's proxy — so nothing is cached, and you spend an afternoon wondering why. Same root cause as the @Transactional self-invocation trap.
One index.html, works offline. If it made Spring caching click, a star helps others find it: https://github.com/dev48v/cache-visualizer
Top comments (0)