Definition
- Persistence context -- the set of managed entity instances that an
EntityManagertracks within a unit of work. - First-level cache (identity map) -- within one context, requesting the same entity by id returns the same Java object instance, so repeated
findcalls hit memory, not the database. - Dirty checking -- at load time Hibernate stores a snapshot of each entity's fields; at flush it compares current state to the snapshot and generates UPDATE SQL only for changed fields.
- Write-behind -- SQL is buffered and flushed at well-defined points (before commit, before an affected query, or on explicit
flush()), letting Hibernate batch and order statements. - Transaction-scoped -- in Spring the context opens when the
@Transactionalmethod begins and closes at commit, after which entities become detached. - Detached entity -- a plain Java object Hibernate no longer tracks; changes to it are not persisted until you
merge()it into a new context.
The persistence context is like Rajesh's working desk for one customer session at National Bank of India. The first time he fetches Priya's account file from the vault he keeps it on the desk (first-level cache). Every further request for that account uses the file already on the desk, not another vault trip. If Rajesh pencils in a balance change, he does not need a separate update form -- at the end of the session he compares the file against the original and automatically files the one change (dirty checking). The desk is cleared when the session (transaction) ends, and the file on the desk becomes a detached photocopy.
Understanding the Concepts
What is the persistence context?
The persistence context is the set of managed entity instances that an EntityManager tracks within a unit of work. Every entity that is loaded (via find, a query, or a relationship traversal) or persisted within the context is said to be in the managed state. While managed, the entity is under Hibernate's watch for dirty checking, identity guarantees, and write-behind.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class AccountService {
private final EntityManager em;
public AccountService(EntityManager em) {
this.em = em;
}
@Transactional
public void demonstratePersistenceContext() {
// Entity loaded into the persistence context -- now MANAGED
Account account = em.find(Account.class, "SAV2001");
// account is tracked; changes will be detected at flush
}
}
Expected behavior:
- em.find() issues SELECT, loads the entity, stores a snapshot
- account is now in the MANAGED state
- Hibernate tracks it for dirty checking until the transaction ends
How does the first-level cache (identity map) work?
The persistence context guarantees that within one unit of work there is at most one object instance per entity id. If you call em.find(Account.class, "SAV2001") twice in the same transaction, the first call hits the database and the second returns the exact same cached instance. The == operator returns true.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class IdentityMapDemo {
private final EntityManager em;
public IdentityMapDemo(EntityManager em) {
this.em = em;
}
@Transactional(readOnly = true)
public void demonstrateIdentityMap() {
Account a1 = em.find(Account.class, "SAV2001"); // SQL SELECT
Account a2 = em.find(Account.class, "SAV2001"); // NO SQL -- returns cached instance
System.out.println("Same instance: " + (a1 == a2));
System.out.println("SQL count: 1 (only the first find hits the DB)");
}
}
Expected output:
Same instance: true
SQL count: 1 (only the first find hits the DB)
How does dirty checking work internally?
When an entity becomes managed, Hibernate takes a snapshot (a copy of its field values). At flush time, Hibernate compares every managed entity's current state against its snapshot. If any field changed, Hibernate generates an UPDATE for that entity. This is why you never call save() on a loaded entity -- changing a field is enough.
package com.bank.service;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class DirtyCheckingDemo {
private final AccountRepository accountRepo;
public DirtyCheckingDemo(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public void freezeAccount(String accountId) {
Account account = accountRepo.findById(accountId).orElseThrow();
// Entity is MANAGED, snapshot taken at load time
account.setStatus("FROZEN");
// No save() needed! Dirty checking detects the change
// at flush and generates: UPDATE accounts SET status='FROZEN' WHERE id=?
}
}
Expected SQL at flush:
UPDATE accounts SET status='FROZEN', holder_name='Priya', balance=50000.00 WHERE id='SAV2001'
-- All columns written by default (see @DynamicUpdate to change this)
How does write-behind and flushing work?
The persistence context does not send SQL the instant you change something. It queues changes and flushes them at well-defined points: before the transaction commits, before a query that might be affected, or when you call flush() explicitly. This write-behind lets Hibernate batch statements and order them correctly.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class WriteBehindDemo {
private final EntityManager em;
public WriteBehindDemo(EntityManager em) {
this.em = em;
}
@Transactional
public void demonstrateWriteBehind() {
Account account = em.find(Account.class, "SAV2001");
account.setBalance(BigDecimal.ZERO);
// No SQL yet -- change is queued
// This query touches the accounts table, so Hibernate auto-flushes first
var zeroBalanceAccounts = em.createQuery(
"SELECT a FROM Account a WHERE a.balance = 0", Account.class)
.getResultList();
// AUTO flush happened before the query, so SAV2001 is in the result
System.out.println("Zero-balance accounts: " + zeroBalanceAccounts.size());
}
}
Expected behavior:
1. em.find() -- SELECT from accounts (entity loaded)
2. setBalance(ZERO) -- change queued, no SQL yet
3. JPQL query -- Hibernate auto-flushes (UPDATE runs), then SELECT
4. SAV2001 appears in the zero-balance results
What is the transaction scope and entity lifecycle?
In a typical Spring application the persistence context is transaction-scoped. It is created when the @Transactional method begins and closed when the transaction commits or rolls back. At that point entities become detached.
package com.bank.service;
import com.bank.model.Account;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class LifecycleDemo {
private final AccountRepository accountRepo;
public LifecycleDemo(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
@Transactional
public Account loadAccount(String id) {
Account account = accountRepo.findById(id).orElseThrow();
// account is MANAGED inside the transaction
return account;
// Transaction commits here, context closes, account becomes DETACHED
}
public void callerMethod() {
Account detached = loadAccount("SAV2001");
// detached.getTransactions() -- LazyInitializationException!
// The persistence context is already closed
}
}
Entity lifecycle states:
1. TRANSIENT -- new Account() -- Hibernate does not know about it
2. MANAGED -- after persist() or find() -- tracked by the context
3. DETACHED -- after transaction closes -- no longer tracked
4. REMOVED -- after remove() -- scheduled for DELETE at flush
What is the difference between managed and detached entities?
A managed entity is tracked by the persistence context. Changes to it are automatically detected and persisted. A detached entity is a plain Java object that Hibernate no longer watches. To persist changes to a detached entity, you must merge it back into a new context.
package com.bank.service;
import com.bank.model.Account;
import jakarta.persistence.EntityManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class MergeDemo {
private final EntityManager em;
public MergeDemo(EntityManager em) {
this.em = em;
}
@Transactional
public Account reattachDetached(Account detached) {
// detached was loaded in a previous transaction
detached.setBalance(new BigDecimal("75000.00"));
// This change is NOT tracked -- detached entity
Account managed = em.merge(detached);
// merge() loads the DB state, copies detached fields over, returns managed copy
// The managed copy's balance change WILL be flushed
return managed;
}
}
Expected behavior:
- em.merge(detached) issues SELECT to load current DB state
- Copies all fields from detached onto the loaded managed entity
- At flush: UPDATE accounts SET balance=75000.00 WHERE id='SAV2001'
- CAUTION: null fields on the detached entity overwrite DB values!
What are common mistakes with the persistence context?
The most common mistake is calling save() on an already-managed entity, which is redundant because dirty checking handles the update. The second is accessing a lazy association after the transaction closes.
// WRONG: redundant save on a managed entity
@Transactional
public void freezeWrong(String id) {
Account a = accountRepo.findById(id).orElseThrow(); // managed
a.setStatus("FROZEN");
accountRepo.save(a); // REDUNDANT -- dirty checking already handles it
}
// CORRECT: just change the field
@Transactional
public void freezeCorrect(String id) {
Account a = accountRepo.findById(id).orElseThrow(); // managed
a.setStatus("FROZEN");
// dirty checking generates UPDATE at commit -- no save() needed
}
// WRONG: lazy access after context closes
public void printTransactions(String id) {
Account a = accountService.loadAccount(id); // returns detached
a.getTransactions().size(); // LazyInitializationException!
}
// CORRECT: fetch eagerly inside the transaction
@Transactional(readOnly = true)
public Account loadWithTransactions(String id) {
return accountRepo.findByIdWithTransactions(id); // JOIN FETCH
}
Common mistakes:
1. Redundant save() on a managed entity
2. LazyInitializationException from accessing lazy associations after TX closes
3. Running outside @Transactional so there is no persistence context at all
4. Unbounded persistence context in a batch loop causing OutOfMemoryError
What should a senior engineer know about the persistence context?
A senior engineer knows that the persistence context has a real memory and CPU cost. Every managed entity holds a strong reference and a snapshot, and dirty checking scans all of them at flush. For big read-only operations, use @Transactional(readOnly = true) to skip snapshots and dirty checking, DTO projections to avoid managed entities entirely, or periodic clear() in batch loops to bound memory.
package com.bank.service;
import com.bank.dto.AccountSummary;
import com.bank.repository.AccountRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class ReadOptimizedService {
private final AccountRepository accountRepo;
public ReadOptimizedService(AccountRepository accountRepo) {
this.accountRepo = accountRepo;
}
// readOnly=true: skip snapshots and dirty checking
@Transactional(readOnly = true)
public List<AccountSummary> getAccountSummaries() {
// DTO projection: never managed, no snapshot, no dirty checking
return accountRepo.findAllProjectedBy();
}
}
Senior checklist:
1. A large persistence context = memory + CPU cost
2. readOnly transactions skip snapshots and dirty checking
3. DTO projections are never managed -- zero context overhead
4. flush() is not commit -- flushed SQL can still be rolled back
5. The persistence context, not the DB, is the source of truth for identity during a TX
6. Extended-scope persistence contexts exist but are rare in stateless web apps
Visual Overview
Entity Lifecycle in the Persistence Context
new Account() persist() flush/commit
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────────┐
│ TRANSIENT │─────►│ MANAGED │─────►│ DATABASE │
│ (new obj) │ │ (tracked)│ │ (persisted) │
└──────────┘ └──────────┘ └──────────────┘
│ ▲
TX closes │ │ merge()
▼ │
┌──────────┐
│ DETACHED │
│(not tracked)│
└──────────┘
│
remove() from managed
▼
┌──────────┐
│ REMOVED │──── flush ──── DELETE from DB
└──────────┘
Dirty Checking at Flush ┌────────────────────────────────────────────┐ │ Persistence Context │ │ │ │ Entity: Account SAV2001 │ │ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Snapshot │ │ Current State │ │ │ │ status=ACTIVE│ │ status=FROZEN │ │ │ │ balance=50K │ │ balance=50K │ │ │ └─────────────┘ └─────────────────────┘ │ │ │ │ │ │ └── compare ─────────┘ │ │ status CHANGED → UPDATE │ │ balance same → skip │ └────────────────────────────────────────────┘
Key Takeaway
The persistence context is the set of managed entity instances that an EntityManager tracks within a unit of work, and it plays three roles. First, it is the first-level cache (identity map): within one context there is at most one object per entity id, so loading the same account twice returns the exact same instance with no SQL on the second call. Second, it performs dirty checking: when an entity becomes managed, Hibernate snapshots its fields and at flush compares current state to the snapshot, generating UPDATE SQL only for changed fields, which is why you never call save() on a loaded entity. Third, it uses write-behind: changes are queued and flushed at well-defined points, letting Hibernate batch and order statements.
The scope is the key to understanding entity lifecycle. In Spring the persistence context is transaction-scoped: it opens when the @Transactional method begins and closes at commit, at which point entities become detached. A detached entity's changes are not tracked until you merge() it into a new context, and accessing a lazy association on a detached entity throws LazyInitializationException because there is no session to load it.
At National Bank of India, Rajesh cleared up real confusion in the team. A developer was redundantly calling save() after changing a managed entity's balance. A test was surprised that two lookups returned the same object. A batch loop ran out of memory because the context grew unbounded with snapshots, which Rajesh fixed by flushing and clearing periodically. For pure reads he switched to @Transactional(readOnly = true) and DTO projections to skip snapshots entirely.
Remember one sentence: the persistence context is the first-level cache, identity map, and dirty-checking engine that tracks managed entities within a transaction and flushes their changes at commit.
---