Question 9 · Section 16

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...

Language versions: English Russian Ukrainian

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

  1. Loading 10k+ entities in one transaction - OutOfMemoryError
  2. Streaming large data - use ScrollableResults
  3. 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]]