Why this matters
- "I called findById twice and saw one SQL" is the first-level cache. Interviewers use it to check whether you understand the persistence context.
- Second-level cache is a frequent follow-up, and the wrong answer is "just put
@Cacheableon the entity." That annotation means two different things in Spring and in JPA. - A stale L2 cache in BookStore serves yesterday's price after an admin update. Knowing invalidation is the production part of the question.
First-level cache
L1 · FresherInside one transaction, loading book 42 twice returns the same Java instance and hits the database once.
Identity map
L2 · IntermediateThe persistence context guarantees one managed instance per id. Two variables pointing at book 42 are ==.
Second-level cache
L3 · AdvancedHibernate can store entity state in a shared cache (Caffeine, Ehcache, Redis via JCache) so the next transaction skips the SELECT.
Query cache caution
L4 · ExpertThe query cache stores id lists, not entities, and is easy to invalidate on every write. Most BookStore workloads should skip it.
First-level cache
// learn:fragment
@Transactional
public void demonstrateL1(Long id) {
Book first = repository.findById(id).orElseThrow();
Book second = repository.findById(id).orElseThrow();
// one SELECT; first == second
first.setPrice(new BigDecimal("12.00"));
// second.getPrice() is already 12.00 — same instance
}
The cache lives on the EntityManager bound to the current transaction. It is not shared with other requests, other threads, or the next transaction.
What the first-level cache guarantees
- Repeatable read of managed instances — Re-reading the same id in the transaction returns the in-memory object, including unsaved changes.
- No extra SELECT — The second
findByIdfor that id does not hit PostgreSQL. - Automatic discard — Commit or rollback closes the persistence context. The next request starts empty.
- Not a performance product feature — It exists for correctness of dirty checking. Do not design APIs around it.
entityManager.clear() drops the first-level cache and detaches every managed entity. Bulk imports do this on a batch boundary so the context does not hold a million books.
entityManager.refresh(book) forces a SELECT and overwrites in-memory state. Use it when a database trigger or another transaction changed the row and you must see the new values inside this transaction.
The rest of this guide is part of Pro
Free covers every fresher guide in full. Pro unlocks Senior, Staff and Architect guides across every track.
Still to come · 4 sections
- First-level cache does not cross transactions
- Second-level cache
- L2 versus Spring Cache
- Query cache
Test yourself
Answer these before moving on — recall is what makes it stick.