Adding a cache is a one-line annotation; keeping it correct is the hard part. Three patterns cover most needs, and each trades freshness against latency differently.
Cache-aside (lazy loading)
The application checks the cache first, loads from the database on a miss, and
populates the cache. This is the most common pattern and what Spring's
@Cacheable implements.
public Product find(Long id) {
Product cached = cache.get(id);
if (cached != null) return cached;
Product loaded = repository.findById(id).orElseThrow();
cache.put(id, loaded);
return loaded;
}Write-through
Writes go to the cache and the database together, so the cache is never stale — at the cost of slower writes.
public Product save(Product product) {
Product saved = repository.save(product);
cache.put(saved.getId(), saved); // cache updated synchronously
return saved;
}Write-behind
Writes hit the cache immediately and flush to the database asynchronously. This is fast but risks data loss if the process dies before the flush.
public void save(Product product) {
cache.put(product.getId(), product);
writeQueue.enqueue(product); // persisted later, in the background
}Takeaways
- Cache-aside is the safe default and matches
@Cacheable. - Write-through keeps the cache fresh but slows writes.
- Write-behind is fastest but sacrifices durability — use it carefully.
Whichever you pick, always set a TTL. Expiry is your backstop for the cache invalidation bugs you haven't found yet.