JPA Caching Explained: Mastering First-Level and Second-Level Caches

Illustration for JPA Caching Explained: Mastering First-Level and Second-Level Caches
By Last updated:

Performance is critical in enterprise applications. Fetching the same data repeatedly from the database is costly and unnecessary. That’s where JPA caching comes into play.

JPA defines two types of caches:

  1. First-Level Cache (EntityManager-level) → Always enabled and mandatory.
  2. Second-Level Cache (Shared, optional) → Must be explicitly enabled, often backed by providers like Ehcache, Infinispan, or Hazelcast.

This tutorial explains how JPA caching works, its configurations, pitfalls, and best practices with code examples.


What is JPA Caching?

JPA caching stores entity data in memory to reduce database hits.

  • First-Level Cache → Scoped to the persistence context (per EntityManager).
  • Second-Level Cache → Shared across sessions/EntityManagers (application-wide).

Analogy:
Think of First-Level Cache as your notebook in class (personal memory), while Second-Level Cache is like the library (shared memory).


First-Level Cache (EntityManager Cache)

  • Enabled by default.
  • Each EntityManager has its own cache.
  • Exists only within a transaction or persistence context.
  • Cannot be disabled.

Example

EntityManager em = entityManagerFactory.createEntityManager();
em.getTransaction().begin();

Employee emp1 = em.find(Employee.class, 1L); // SQL executed
Employee emp2 = em.find(Employee.class, 1L); // Retrieved from cache, no SQL

em.getTransaction().commit();
em.close();

Here, the second find() does not hit the database because the entity is cached in the persistence context.


Second-Level Cache (Shared Cache)

  • Optional and must be enabled explicitly.
  • Shared across multiple EntityManager instances.
  • Requires a JPA provider that supports it (e.g., Hibernate).

Example Configuration (Hibernate + Ehcache)

persistence.xml:

<persistence-unit name="examplePU">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <properties>
        <property name="hibernate.cache.use_second_level_cache" value="true"/>
        <property name="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
        <property name="hibernate.cache.use_query_cache" value="true"/>
    </properties>
</persistence-unit>

Entity with Second-Level Cache

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

@Entity
@Table(name = "employees")
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String department;
    private Double salary;
}

Query Cache in JPA

Besides entity caching, JPA providers like Hibernate also support query result caching.

List<Employee> employees = entityManager
    .createQuery("SELECT e FROM Employee e WHERE e.department = :dept", Employee.class)
    .setParameter("dept", "HR")
    .setHint("org.hibernate.cacheable", true)
    .getResultList();

CRUD Example with Cache

Insert (Persist)

em.getTransaction().begin();
Employee emp = new Employee();
emp.setName("Alice");
emp.setDepartment("Finance");
emp.setSalary(90000.0);
em.persist(emp);
em.getTransaction().commit();

Read (First-Level Cache Demonstration)

Employee e1 = em.find(Employee.class, emp.getId()); // SQL executed
Employee e2 = em.find(Employee.class, emp.getId()); // From cache

Update

em.getTransaction().begin();
emp.setSalary(95000.0);
em.merge(emp); // Cache invalidated/updated
em.getTransaction().commit();

Delete

em.getTransaction().begin();
em.remove(emp); // Entity removed from cache
em.getTransaction().commit();

Real-World Use Cases

  • First-Level Cache → Transaction-scoped optimizations (avoid duplicate queries).
  • Second-Level Cache → Read-heavy applications (e.g., product catalogs).
  • Query Cache → Repeated reporting queries.

Anti-Patterns and Pitfalls

  • Blindly enabling second-level cache → may cause stale data issues.
  • Caching write-heavy entities → increases overhead.
  • Forgetting cache invalidation → can serve outdated results.
  • Overusing query cache → may increase memory pressure.

Best Practices

  • Use First-Level Cache as is (default).
  • Enable Second-Level Cache only for read-mostly entities.
  • Combine caching with optimistic locking for consistency.
  • Use query cache selectively, not globally.
  • Monitor cache hit/miss ratio in production.

📌 JPA Version Notes

  • JPA 2.0 → Introduced Criteria API and type-safe queries.
  • JPA 2.1 → Added entity graphs, stored procedures.
  • JPA 2.2 → Improved Java 8 Date/Time API support.
  • Jakarta Persistence (EE 9/10/11) → Package renamed from javax.persistencejakarta.persistence.

Conclusion and Key Takeaways

  • First-Level Cache → Always enabled, per persistence context.
  • Second-Level Cache → Optional, shared, requires explicit configuration.
  • Query Cache → Useful for repeated queries, but should be used cautiously.
  • Proper caching strategy leads to fewer database hits, better performance, and scalable systems.

FAQ: Expert-Level Questions

1. What’s the difference between JPA and Hibernate?
JPA is a specification; Hibernate is a popular implementation.

2. How does JPA handle the persistence context?
It works like a classroom attendance register, tracking entities.

3. What are the drawbacks of eager fetching in JPA?
It loads unnecessary data, which can bypass cache benefits.

4. How can I solve the N+1 select problem with JPA?
Use JOIN FETCH, batch fetching, or entity graphs.

5. Can I use JPA without Hibernate?
Yes, implementations like EclipseLink and OpenJPA are available.

6. What’s the best strategy for inheritance mapping in JPA?
Choose between SINGLE_TABLE, JOINED, or TABLE_PER_CLASS based on needs.

7. How does JPA handle composite keys?
By using @IdClass or @EmbeddedId annotations.

8. What changes with Jakarta Persistence?
Package renamed to jakarta.persistence.

9. Is JPA suitable for microservices?
Yes, but cache should be designed carefully to avoid stale data across services.

10. When should I avoid using JPA?
In batch-heavy, analytics-heavy, or ultra-low latency systems.