Full Stack • Java • System Design • Cloud • AI Engineering

Hibernate2026-06-17

Hibernate Caching

Master Hibernate caching with Mermaid diagrams covering first-level cache, second-level cache, query cache, and performance optimization strategies

Hibernate Caching is a powerful mechanism to improve application performance by reducing database queries. Understanding the different cache levels and their behavior is crucial for building high-performance applications.

Hibernate Cache Levels

graph TB
    A[Hibernate Caching] --> B[First-Level Cache]
    A --> C[Second-Level Cache]
    A --> D[Query Cache]
    
    B --> B1[Session-scoped]
    B --> B2[Always enabled]
    B --> B3[Transaction-level]
    
    C --> C1[SessionFactory-scoped]
    C --> C2[Optional]
    C --> C3[Application-level]
    
    D --> D1[Query results]
    D --> D2[Requires 2nd level]
    D --> D3[Explicit enable]
    
    style A fill:#2196F3
    style B fill:#4CAF50
    style C fill:#FF9800
    style D fill:#9C27B0

Key Points:

  • First-Level Cache: Session-scoped, mandatory, transaction-level caching
  • Second-Level Cache: SessionFactory-scoped, optional, application-level caching
  • Query Cache: Caches query results, requires second-level cache
  • Performance: Reduces database round trips significantly
  • Configuration: First-level automatic, others need explicit configuration

First-Level Cache Architecture

sequenceDiagram
    participant App as Application
    participant Session as Hibernate Session
    participant Cache as First-Level Cache
    participant DB as Database
    
    App->>Session: load(Entity.class, id)
    Session->>Cache: Check cache
    
    alt Cache Hit
        Cache->>Session: Return cached entity
        Session->>App: Return entity
    else Cache Miss
        Session->>DB: SELECT query
        DB->>Session: Return data
        Session->>Cache: Store in cache
        Session->>App: Return entity
    end
    
    Note over Cache: Cache cleared on session.close()

Key Points:

  • Automatic: Always enabled, cannot be disabled
  • Scope: Lives within a single Session/Transaction
  • Lifecycle: Cleared when session is closed or cleared
  • Purpose: Reduces queries within same transaction
  • Flush: Synchronizes cache with database before query execution

First-Level Cache Code Example

// First-level cache demonstration
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

// First load - hits database
Employee emp1 = session.get(Employee.class, 1L);
System.out.println("First load: " + emp1.getName());

// Second load - hits first-level cache (no DB query)
Employee emp2 = session.get(Employee.class, 1L);
System.out.println("Second load: " + emp2.getName());

// Both references point to same object
System.out.println(emp1 == emp2); // true

// Modify entity
emp1.setSalary(75000);

// Changes not yet in database
// flush() synchronizes cache with DB
session.flush();

// Commit writes to database
tx.commit();
session.close();

// Output:
// Hibernate: select employee0_.id, employee0_.name, employee0_.salary from Employee employee0_ where employee0_.id=?
// First load: John Doe
// Second load: John Doe
// true
// Hibernate: update Employee set name=?, salary=? where id=?

When to Explicitly Call flush()

// Use case 1: Get generated ID immediately
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Employee newEmp = new Employee("Jane Smith", 60000);
session.save(newEmp);

// Flush to get auto-generated ID
session.flush();
Long generatedId = newEmp.getId(); // Now available
System.out.println("Generated ID: " + generatedId);

tx.commit();
session.close();

// Use case 2: Batch processing to conserve memory
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

for (int i = 0; i < 10000; i++) {
    Employee emp = new Employee("Employee " + i, 50000);
    session.save(emp);
    
    // Flush and clear every 50 records
    if (i % 50 == 0) {
        session.flush();  // Write to DB
        session.clear();  // Clear cache to free memory
    }
}

tx.commit();
session.close();

Second-Level Cache Architecture

graph TB
    A[Application] --> B[Session 1]
    A --> C[Session 2]
    A --> D[Session 3]
    
    B --> E[First-Level Cache]
    C --> F[First-Level Cache]
    D --> G[First-Level Cache]
    
    E --> H[Second-Level Cache]
    F --> H
    G --> H
    
    H --> I[SessionFactory]
    I --> J[Database]
    
    style H fill:#FF9800
    style I fill:#2196F3
    style J fill:#4CAF50

Key Points:

  • Shared: Accessible across all sessions in application
  • SessionFactory-scoped: Lives as long as SessionFactory
  • Optional: Must be explicitly configured
  • Provider: EhCache, Infinispan, Hazelcast, Redis
  • Granular: Can cache specific entities, collections

Second-Level Cache Configuration

// hibernate.cfg.xml configuration
<hibernate-configuration>
    <session-factory>
        <!-- Enable second-level cache -->
        <property name="hibernate.cache.use_second_level_cache">true</property>
        
        <!-- Cache provider (EhCache) -->
        <property name="hibernate.cache.region.factory_class">
            org.hibernate.cache.ehcache.EhCacheRegionFactory
        </property>
        
        <!-- Enable query cache -->
        <property name="hibernate.cache.use_query_cache">true</property>
        
        <!-- Show cache statistics -->
        <property name="hibernate.generate_statistics">true</property>
    </session-factory>
</hibernate-configuration>
<!-- ehcache.xml configuration -->
<ehcache>
    <defaultCache
        maxElementsInMemory="1000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="false"
        memoryStoreEvictionPolicy="LRU"/>
    
    <!-- Cache for Employee entity -->
    <cache name="com.example.Employee"
        maxElementsInMemory="500"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="false"/>
    
    <!-- Cache for Department entity -->
    <cache name="com.example.Department"
        maxElementsInMemory="100"
        eternal="true"
        overflowToDisk="false"/>
</ehcache>

Entity-Level Cache Configuration

// Enable caching on entity with annotations
@Entity
@Table(name = "employees")
@Cacheable
@org.hibernate.annotations.Cache(
    usage = CacheConcurrencyStrategy.READ_WRITE
)
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    private Double salary;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "department_id")
    private Department department;
    
    // Getters and setters
}

// Or using XML mapping
<class name="com.example.Employee" table="employees">
    <cache usage="read-write"/>
    <id name="id" column="id">
        <generator class="identity"/>
    </id>
    <property name="name" column="name"/>
    <property name="salary" column="salary"/>
</class>

Cache Concurrency Strategies

graph LR
    A[Cache Strategies] --> B[READ_ONLY]
    A --> C[READ_WRITE]
    A --> D[NONSTRICT_READ_WRITE]
    A --> E[TRANSACTIONAL]
    
    B --> B1[Never updated]
    B --> B2[Best performance]
    
    C --> C1[Read and write]
    C --> C2[Soft locks]
    
    D --> D1[Occasional updates]
    D --> D2[No guarantees]
    
    E --> E1[JTA only]
    E --> E2[Full ACID]
    
    style A fill:#2196F3
    style B fill:#4CAF50
    style C fill:#FF9800
    style D fill:#9C27B0
    style E fill:#F44336

Key Points:

  • READ_ONLY: For immutable data, best performance
  • READ_WRITE: For frequently updated data, uses soft locks
  • NONSTRICT_READ_WRITE: For occasionally updated data
  • TRANSACTIONAL: Full transactional support, JTA required
  • Choice: Based on data update frequency and consistency needs

Second-Level Cache Code Example

// Demonstrating second-level cache
SessionFactory sessionFactory = // ... initialized

// Session 1: Load entity (cache miss, hits DB)
Session session1 = sessionFactory.openSession();
Employee emp1 = session1.get(Employee.class, 1L);
System.out.println("Session 1: " + emp1.getName());
session1.close();
// Hibernate: select ... from Employee where id=?

// Session 2: Load same entity (cache hit, no DB query)
Session session2 = sessionFactory.openSession();
Employee emp2 = session2.get(Employee.class, 1L);
System.out.println("Session 2: " + emp2.getName());
session2.close();
// No SQL query - retrieved from second-level cache

// Session 3: Update entity
Session session3 = sessionFactory.openSession();
Transaction tx = session3.beginTransaction();
Employee emp3 = session3.get(Employee.class, 1L);
emp3.setSalary(80000);
tx.commit();
session3.close();
// Cache is invalidated/updated

// Session 4: Load entity (cache hit with updated data)
Session session4 = sessionFactory.openSession();
Employee emp4 = session4.get(Employee.class, 1L);
System.out.println("Session 4 salary: " + emp4.getSalary()); // 80000
session4.close();

Query Cache

sequenceDiagram
    participant App as Application
    participant Session as Session
    participant QCache as Query Cache
    participant 2ndCache as 2nd Level Cache
    participant DB as Database
    
    App->>Session: createQuery().setCacheable(true)
    Session->>QCache: Check query cache
    
    alt Query Cache Hit
        QCache->>2ndCache: Get entity IDs
        2ndCache->>Session: Return entities
        Session->>App: Return results
    else Query Cache Miss
        Session->>DB: Execute query
        DB->>Session: Return results
        Session->>QCache: Cache query results (IDs)
        Session->>2ndCache: Cache entities
        Session->>App: Return results
    end

Key Points:

  • Query Results: Caches query result sets (entity IDs)
  • Dependency: Requires second-level cache enabled
  • Explicit: Must call setCacheable(true) on query
  • Invalidation: Invalidated when table data changes
  • Use Case: Frequently executed queries with same parameters

Query Cache Code Example

// Enable query cache in configuration
// hibernate.cache.use_query_cache=true

Session session = sessionFactory.openSession();

// Query 1: Cache miss, hits database
Query<Employee> query1 = session.createQuery(
    "FROM Employee WHERE salary > :minSalary", Employee.class);
query1.setParameter("minSalary", 50000.0);
query1.setCacheable(true); // Enable caching for this query
List<Employee> employees1 = query1.list();
System.out.println("Query 1 results: " + employees1.size());

// Query 2: Same query, cache hit (no DB query)
Query<Employee> query2 = session.createQuery(
    "FROM Employee WHERE salary > :minSalary", Employee.class);
query2.setParameter("minSalary", 50000.0);
query2.setCacheable(true);
List<Employee> employees2 = query2.list();
System.out.println("Query 2 results: " + employees2.size());

session.close();

// Output:
// Hibernate: select ... from Employee where salary>?
// Query 1 results: 15
// Query 2 results: 15 (no SQL query)

// Named query with cache
@NamedQuery(
    name = "Employee.findBySalaryRange",
    query = "FROM Employee WHERE salary BETWEEN :min AND :max",
    hints = @QueryHint(name = "org.hibernate.cacheable", value = "true")
)
public class Employee {
    // ...
}

// Using named query
List<Employee> employees = session
    .createNamedQuery("Employee.findBySalaryRange", Employee.class)
    .setParameter("min", 40000.0)
    .setParameter("max", 80000.0)
    .list();

Cache Performance Monitoring

// Enable statistics
Configuration config = new Configuration();
config.setProperty("hibernate.generate_statistics", "true");

// Get statistics
SessionFactory sessionFactory = config.buildSessionFactory();
Statistics stats = sessionFactory.getStatistics();

// Perform operations
Session session = sessionFactory.openSession();
// ... perform database operations
session.close();

// Print cache statistics
System.out.println("Second Level Cache Hits: " + 
    stats.getSecondLevelCacheHitCount());
System.out.println("Second Level Cache Misses: " + 
    stats.getSecondLevelCacheMissCount());
System.out.println("Second Level Cache Puts: " + 
    stats.getSecondLevelCachePutCount());

System.out.println("Query Cache Hits: " + 
    stats.getQueryCacheHitCount());
System.out.println("Query Cache Misses: " + 
    stats.getQueryCacheMissCount());

System.out.println("Query Execution Count: " + 
    stats.getQueryExecutionCount());

// Cache hit ratio
double hitRatio = (double) stats.getSecondLevelCacheHitCount() / 
    (stats.getSecondLevelCacheHitCount() + stats.getSecondLevelCacheMissCount());
System.out.println("Cache Hit Ratio: " + (hitRatio * 100) + "%");

Cache Eviction and Management

// Manual cache eviction
SessionFactory sessionFactory = // ... initialized

// Evict specific entity from cache
sessionFactory.getCache().evict(Employee.class, 1L);

// Evict all instances of an entity
sessionFactory.getCache().evict(Employee.class);

// Evict entire second-level cache
sessionFactory.getCache().evictAllRegions();

// Check if entity is in cache
boolean inCache = sessionFactory.getCache()
    .contains(Employee.class, 1L);

// Evict query cache
sessionFactory.getCache().evictQueryRegions();

// Evict specific query cache region
sessionFactory.getCache().evictQueryRegion("myQueryRegion");

Best Practices

  1. Use Second-Level Cache for Read-Heavy Data: Entities that are frequently read but rarely updated
  2. Choose Appropriate Strategy: READ_ONLY for immutable, READ_WRITE for mutable data
  3. Monitor Cache Statistics: Track hit/miss ratios to optimize cache configuration
  4. Set Appropriate TTL: Configure time-to-live based on data staleness tolerance
  5. Avoid Caching Large Objects: Can cause OutOfMemoryError
  6. Use Query Parameters: Don't cache entire objects in query parameters
  7. Enable Batch Processing: Use flush() and clear() for large batch operations
  8. Test Cache Behavior: Verify cache hits/misses in development
// Good practice: Use IDs in queries
String hql = "FROM Order WHERE customer.id = :customerId";
Query<Order> query = session.createQuery(hql, Order.class);
query.setParameter("customerId", 123L);
query.setCacheable(true);

// Bad practice: Using entire object
// String hql = "FROM Order WHERE customer = :customer";
// query.setParameter("customer", customerObject); // Caches entire object

Common Pitfalls

  1. Stale Data: Cache not invalidated when data updated outside Hibernate
  2. Memory Issues: Over-caching can cause OutOfMemoryError
  3. Cache Coherence: Multiple application instances need distributed cache
  4. Query Cache Invalidation: Entire cache invalidated on any table update
  5. Association Caching: Requires fetch="select" for second-level cache to work