Decorator for caching
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
A caching repository decorator implements the same contract and wraps the real repository. For example, findById(42) first checks a cache, delegates to the database on a miss, and stores a successful result. On a database write, a simple policy is to evict key 42 only after the transaction succeeds, so the next read reloads current data. I do not claim this makes the cache automatically consistent: the database and cache are separate failure domains, and concurrent writers or multiple application instances can still produce stale values. ConcurrentHashMap protects its own operations but adds no TTL, size bound, distributed invalidation, or stampede control. A production design needs those policies or a suitable cache library.
Decorator for caching
Direct Answer
A caching repository decorator implements the repository contract, wraps the real repository, returns a cached value on a valid hit, and delegates on a miss. Writes usually evict or update affected keys only after the database succeeds, but database and cache changes are not atomic, so stale data, concurrent misses, failure recovery, TTL, and multi-instance invalidation need an explicit policy.
Code Example
final class CachingUserRepository implements UserRepository {
private final UserRepository delegate;
private final ConcurrentMap<Long, User> cache = new ConcurrentHashMap<>();
CachingUserRepository(UserRepository delegate) {
this.delegate = delegate;
}
public Optional<User> findById(Long id) {
User cached = cache.get(id);
if (cached != null) return Optional.of(cached);
Optional<User> loaded = delegate.findById(id);
loaded.ifPresent(user -> cache.put(id, user));
return loaded;
}
public User save(User user) {
User saved = delegate.save(user); // transaction must succeed first
cache.remove(saved.id()); // reload on the next read
return saved;
}
public void deleteById(Long id) {
delegate.deleteById(id);
cache.remove(id);
}
}
This is a teaching example, not a complete production cache: it has no TTL, size bound, cross-instance invalidation, or stampede control.
Common Mistakes & Pitfalls
- Calling a concurrent map a complete cache: it has no expiry, size bound, or multi-instance coherence.
- Assuming write-through is atomic: the database can succeed while the cache update fails.
- Using
containsKeyfollowed byget: a concurrent removal can change the result between calls; read once. - Caching mutable entities directly: later mutation can silently change the cached value.
- Ignoring misses and stampedes: decide whether absence is cached and how concurrent loads are combined.
Check your understanding
Answer these without looking back at the article.
- How does Spring's @Cacheable compare to a manual Decorator-based cache?
- How would you handle cache invalidation in a Decorator-based caching layer?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.