What is the First-Level Cache in Hibernate
The first-level cache (L1 cache) is a built-in cache at the EntityManager/Session level. It is a fundamental part of Hibernate and provides entity identity, dirty checking, and...
Overview
The first-level cache (L1 cache) is a built-in cache at the EntityManager/Session level. It is a fundamental part of Hibernate and provides entity identity, dirty checking, and query optimization.
Why: L1 cache solves three problems: (1) Identity guarantee - one ID = one object in memory; (2) Dirty checking - needs to store a snapshot for comparison; (3) Optimization - repeated find() doesn’t do SELECT.
Junior Level
What is L1 Cache
The first-level cache is a cache inside the EntityManager (session). All loaded entities are stored in it automatically.
// First request - SELECT from DB
User user1 = entityManager.find(User.class, 1L);
// SQL: SELECT * FROM users WHERE id = 1
// Second request - from cache, NO SELECT!
User user2 = entityManager.find(User.class, 1L);
// SQL: (no query!)
assert user1 == user2; // true - same object in memory
Key Properties
- Enabled by default, cannot be disabled
- Lives within one session/transaction
- Automatically cleared when EntityManager closes
- Provides identity guarantee (one ID = one object). If you load User(1) twice in one transaction, Hibernate returns THE SAME object in memory (user1 == user2). This is needed for dirty checking to work correctly - otherwise changes to one object could conflict with another.
When It Triggers
// find() - checks cache before DB query
User user1 = entityManager.find(User.class, 1L); // SELECT
User user2 = entityManager.find(User.class, 1L); // from cache
// persist() - adds to cache
User newUser = new User();
entityManager.persist(newUser); // in cache
// merge() - adds updated copy to cache
User managed = entityManager.merge(detachedUser); // in cache
Middle Level
When L1 Cache is Harmful
- Loading 10k+ entities in one transaction - OutOfMemoryError
- Streaming large data - use ScrollableResults
- Batch operations - use StatelessSession
Dirty Checking via L1 Cache
// Hibernate saves snapshot on load
User user = entityManager.find(User.class, 1L); // snapshot created
// Change a field
user.setName("New Name");
// On commit/flush - Hibernate compares snapshot with current state
// If different - automatically does UPDATE
// No explicit entityManager.merge() needed!
Cache Management
// Clear entire cache
entityManager.clear();
// All entities become detached
// Remove specific entity from cache
entityManager.detach(user);
// user becomes detached, others remain
// Reload from DB (update cache)
entityManager.refresh(user);
// SELECT from DB, snapshot updated
// Check presence in cache
boolean isManaged = entityManager.contains(user);
The Big L1 Cache Problem
// Loaded 100k entities -> OutOfMemoryError!
for (Long id : allIds) {
entityManager.find(User.class, id); // all in cache
}
// Solution: periodic cleanup
for (int i = 0; i < allIds.size(); i++) {
entityManager.find(User.class, allIds.get(i));
if (i % 100 == 0) {
entityManager.flush(); // flush to DB
entityManager.clear(); // clear cache
}
}
Common Mistakes
// Long-lived transaction with large cache
@Transactional
public void processAll() {
List<User> users = userRepository.findAll(); // 50k users
for (User user : users) {
process(user); // L1 cache grows
}
// OutOfMemoryError!
}
// Solution: batch processing
@Transactional
public void processAll() {
List<User> users = userRepository.findAll();
for (int i = 0; i < users.size(); i++) {
process(users.get(i));
if (i % 100 == 0) {
entityManager.flush();
entityManager.clear();
}
}
}
Senior Level
L1 Cache vs Manual Map<String, User>
Unlike a manual Map, L1 cache: (1) is automatically populated on any query; (2) provides identity guarantee; (3) stores snapshot for dirty checking; (4) is automatically cleared when EntityManager closes.
Internal Implementation
StatefulPersistenceContext {
// Map<EntityKey, Entity> - cache by ID
entitiesByKey: Map<EntityKey, Object>
// Map<Object, EntityEntry> - state tracking
entityEntries: Map<Object, EntityEntry>
}
EntityEntry contains:
- loadedState - snapshot at load
- status - MANAGED, DELETED, READ_ONLY
- id - identifier
- version - for optimistic locking
find() Algorithm
entityManager.find(User.class, 1L):
1. Create EntityKey(User, 1L)
2. Check entitiesByKey.containsKey(key)
-> Yes: return object from cache (NO SQL)
-> No: execute SELECT
3. SELECT result -> create object
4. Add to entitiesByKey
5. Create EntityEntry with snapshot
6. Return object
Read-Only Hint for Optimization
// For entities that won't be modified
List<User> users = entityManager.createQuery("FROM User", User.class)
.setHint("org.hibernate.readOnly", true)
.getResultList();
// If you modify a read-only entity, dirty checking will NOT detect changes
// and UPDATE will not be executed. Hibernate simply ignores the changes.
// Advantages:
// 1. No snapshot created (memory savings)
// 2. Dirty checking not executed
// 3. Less flush overhead
Performance Characteristics
L1 Cache overhead:
- O(1) for find() (HashMap lookup)
- O(N) for dirty checking (traversing all entities)
- O(N) memory (storing snapshot for each entity)
For large operations:
- flush() + clear() every 50-100 entities
- read-only hint when no modification needed
- StatelessSession for bulk operations
Best Practices
L1 cache works by default
entityManager.clear() for batch operations
entityManager.detach() for individual objects
refresh() for DB reload
read-only hint for read-only queries
flush + clear every 50-100 entities in batch
Loading large number of entities without clear
Ignoring OutOfMemoryError during large operations
Long-lived transactions with large cache
Using L1 cache as inter-request cache
Interview Cheat Sheet
Must know:
- L1 cache - built-in EntityManager cache, enabled by default, cannot be disabled
- Solves 3 problems: identity guarantee, dirty checking (snapshot), query optimization
- find() checks cache before DB query - repeated find() without SQL
- Lives within one transaction, cleared when EntityManager closes
- When loading 10k+ entities - OutOfMemoryError, need clear()
- Read-only hint saves memory (no snapshot created)
Frequent follow-up questions:
- Can L1 cache be disabled? No, it’s a fundamental part of Hibernate
- Why doesn’t repeat find() do SELECT? Object already in entitiesByKey Map
- How to avoid OOM in batch? flush() + clear() every 50-100 entities
- L1 cache vs manual Map? L1 cache auto-populated, provides identity guarantee, stores snapshot
Red flags (DO NOT say):
- “I use L1 cache as inter-request cache” - it only lives within one session
- “I disable L1 cache for performance” - cannot be disabled
- “I load 100k entities without clear” - OutOfMemoryError
- “L1 cache replaces L2 cache” - different levels with different purposes
Related topics:
- [[10. What is the Second-Level Cache and When to Use It]]
- [[11. How to Configure the Second-Level Cache]]
- [[12. What is Dirty Checking in Hibernate]]
- [[13. How Does the Flush Mechanism Work in Hibernate]]