Command Palette

Search for a command to run...

PHASE 10Intermediate ~7 min· topic 2 of 16

Topic 10.2

Cache-aside (Lazy Loading)

In one line

The app checks the cache, loads from the DB on miss, and writes the cache itself — the default strategy.

0/16 · 0%

Think of it like this

Checking your fridge first before going to the store. If the milk's there (cache hit), great, done. If not (cache miss), you go to the store (database), buy it, AND put a copy in the fridge for next time.

Key ideas

  1. 01

    Read path: get(key) → hit? done : DB load → put cache → return.

  2. 02

    Advantages: cache only holds what's actually read; simple; cache holds app-specific shapes.

  3. 03

    Risk #1: cache stampede on miss (see the blog post on the subject) → single-flight + TTL jitter.

  4. 04

    Risk #2: stale cache on write → evict on write (@CacheEvict) or TTL-bounded staleness.

  5. 05

    Write path rule: invalidate on write (delete key), DON'T write-through on every write — one less race.

  6. 06

    Interview: 'cache-aside with eviction on write and single-flight on miss' is a complete sentence.

Java / Spring map

  • →

    The full LoadingCache single-flight code lives in phase-5 → concurrent-cache.

Code & diagrams

cache-aside read and write pathsdiagram
Rendering diagram…
CacheAside.javajava

The read/write path with eviction — the canonical pair.

@Service
public class ProfileService {
  private final ProfileRepository db;
  private final StringRedisTemplate redis;     // cache-aside on Redis

  private static final String KEY = "profile:";

  public Profile get(String userId) {
    String json = redis.opsForValue().get(KEY + userId);   // 1. check cache
    if (json != null) return Profile.fromJson(json);        // hit → done

    Profile p = db.findById(userId).orElseThrow();          // 2. DB miss path
    redis.opsForValue().set(KEY + userId, p.toJson(), Duration.ofMinutes(15)); // 3. populate
    return p;
  }

  @CacheEvict(value = "profiles", key = "#userId")          // 4. evict on write
  public Profile update(String userId, Profile newData) {
    return db.save(newData);
  }
}

Explain without notes

01

Show the race: two readers miss, both load, both write — why is that mostly harmless here, and when is it NOT?

Practice

01

Add TTL jitter and single-flight to the get() and justify each in a comment.

Trade-offs

  • ↔

    App-managed because the app owns the shapes; teams must not forget the eviction half.

Run it in production

You've designed it. Now build, operate, and break the same idea hands-on in the DevOps courses:

Completion checklist

  • I treat cache-aside as: check, miss, load, populate, and EVICT ON WRITE.

Back to phase