← Back to the section

Hibernate can keep loaded data in memory so it doesn't have to hit the database again. This helps — but only if you understand which cache is actually working, where its boundaries are, and when it can let you down.

First-level cache — the one that's always on

The first-level cache is the Session itself (or the EntityManager in JPA terms). While the session is open, every loaded entity lives in its persistence context. A repeated find() for the same identifier won't hit the database — Hibernate returns the already-loaded object.

EntityManager em = emf.createEntityManager();

Product p1 = em.find(Product.class, 42L); // SELECT ... WHERE id = 42
Product p2 = em.find(Product.class, 42L); // no query — served from the session cache

System.out.println(p1 == p2); // true: it's the same object

This isn't only about saving queries — it's also a consistency guarantee within a single session: you always work with one instance of an object, not with independent copies.

Boundaries of the first-level cache

The cache lives exactly as long as the session lives. As soon as the session is closed, everything that was in memory disappears. The next session starts from a clean slate and goes back to the database.

// Session 1
EntityManager em1 = emf.createEntityManager();
Product p = em1.find(Product.class, 42L); // SELECT
em1.close();

// Session 2 — the first-level cache is already empty
EntityManager em2 = emf.createEntityManager();
Product p2 = em2.find(Product.class, 42L); // SELECT again

One more nuance: JPQL queries, even when they return already-loaded entities, do not use the first-level cache for filtering — they always hit the database. Hibernate then reconciles the result with what's already in the session and substitutes the existing objects for the new ones.

For a detailed look at how the persistence context is built, see the article /hibernate/persistence-context/.

Second-level cache — between sessions

The second-level cache works at the SessionFactory / EntityManagerFactory level. It survives the closing of individual sessions and is available to all of them. It's an optional feature — off by default.

Typical providers: Ehcache and Infinispan. The setup is wired in through persistence.xml or application.yml:

spring:
  jpa:
    properties:
      hibernate.cache.use_second_level_cache: true
      hibernate.cache.region.factory_class: org.hibernate.cache.jcache.internal.JCacheRegionFactory
      javax.cache.provider: org.ehcache.jsr107.EhcacheCachingProvider

For an entity to be cached in the second level, you have to mark it explicitly:

import jakarta.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;

@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Country {

    @Id
    private Long id;

    private String name;
}

After that, find(Country.class, 1L) from any session will first check the second-level cache and only go to the database on a miss.

Concurrency strategies

Hibernate offers several strategies through CacheConcurrencyStrategy:

StrategyWhen to use
READ_ONLYData never changes (reference tables, enumerations)
READ_WRITEData changes; you need consistency on update
NONSTRICT_READ_WRITERare updates; a small staleness window is acceptable
TRANSACTIONALFull transactional isolation is required (JTA only)

When the second-level cache helps

Good candidates for caching:

  • Reference tables — countries, currencies, product categories — data that changes once a month and is read thousands of times a day.
  • Settings — configuration records that get loaded on every request.
  • Aggregates with wide reads — entities that see many find() calls and few merge()/remove() calls.

Short formula: the second-level cache pays off when the read-to-write ratio is high and the data doesn't change too often.

When the second-level cache hurts

This is exactly where the unexpected problems begin.

Stale data on external changes

Hibernate invalidates the cache only when it itself performs the change through the EntityManager. If the data in the database was changed by another application, a migration script, or direct SQL — the cache won't know about it and will keep serving old values.

// Another process ran: UPDATE product SET price = 999 WHERE id = 5

// Our code gets the old price from the second-level cache
Product p = em.find(Product.class, 5L); // price from the cache, not from the database!

Distributed systems and multiple nodes

In a cluster, each node has its own JVM. A local Ehcache on node A doesn't know about changes that went through node B. For the second-level cache to work correctly in a cluster, you need a distributed provider (Infinispan in cluster mode, Redis through a third-party adapter). That's a separate piece of infrastructure with extra complexity.

Large mutable graphs

If an entity is updated often, then on every merge() or remove() Hibernate evicts it from the cache. As a result, the cache almost always misses (cache miss), while the serialization/deserialization overhead only adds latency. In this case it's better not to enable the second-level cache at all.

Query cache

Besides caching by identifier, Hibernate can cache the results of JPQL/HQL queries. It's enabled separately:

spring:
  jpa:
    properties:
      hibernate.cache.use_query_cache: true

And marked explicitly on each query:

List<Country> countries = em.createQuery("FROM Country ORDER BY name", Country.class)
        .setHint("org.hibernate.cacheable", true)
        .getResultList();

The query cache stores not the objects themselves but a list of identifiers. The objects are then fetched from the second-level cache (or the database). That's why the query cache without the second-level cache is almost useless — it saves one query for the list, but still goes to the database for each entity.

Query cache invalidation

The query cache is invalidated entirely for a table on any change to any entity from it. If a table changes often, the query cache against it won't give any noticeable benefit.

// Someone saved a new country
em.persist(new Country("New Zealand", "NZ"));
em.flush();

// The entire query cache for the Country table is cleared
// The next query goes to the database again

When it's better not to enable the cache at all

There are situations where caching at the Hibernate level only gets in the way:

  • High write frequency — invalidations happen more often than cache hits.
  • Multiple applications sharing a database — Hibernate doesn't know about changes from the neighboring applications.
  • Strict consistency requirements — where you can't allow even a short window of stale data.
  • Analytical queries — complex aggregates over large tables are better optimized with indexes and materialized views at the database level, rather than hidden behind an ORM cache.

In such cases it's worth looking toward application-level caching (Spring Cache + Redis) or optimizing the queries themselves. For database-level transactions and locking, see the article /postgres/transactional-spring/.

In short

  • The first-level cache is the persistence context, always on, lives within a single session; a repeated find() for the same ID doesn't hit the database.
  • The second-level cache is optional, at the SessionFactory level, survives the closing of sessions; it requires an explicit @Cache annotation on the entity.
  • Good candidates for the second level are rarely changed reference tables with a high read-to-write ratio.
  • The second-level cache doesn't see changes from external sources (direct SQL, other applications) and is complex in a cluster.
  • The query cache stores lists of identifiers, works together with the second level, and is reset on any change in the table.
  • If data changes often or multiple applications share the database — it's better not to enable the second-level cache.
  • Persistence context and the entity lifecycle
  • The N+1 query problem
  • Transactions and locking in Hibernate
  • Spring Data JPA: repositories on top of Hibernate