intermediate~2h

Persistence Context & Dirty Checking

You've never called.save() after modifying a loaded entity, and it still worked. This chapter is why — and it's one of the most-asked internals questions in mid-to-senior Java interviews.

@Transactional public void renameProduct(Long id, String newName) { Product product = productRepository.findById(id).orElseThrow(); product.setName(newName); // a plain setter call — nothing database-related here at all // method ends. No save(). No update query written anywhere. // Yet the database row genuinely gets updated. How? }

💻 Code example

@Transactional public void renameProduct(Long id, String newName) { Product product = productRepository.findById(id).orElseThrow(); product.setName(newName); // a plain setter call — nothing database-related here at all // method ends. No save(). No update query written anywhere. // Yet the database row genuinely gets updated. How? }

The persistence context — sometimes called the first-level cache — is a per- EntityManager (per-transaction, in the typical Spring setup) tracking structure that remembers every entity it has loaded or saved during this unit of work, along with a snapshot of each entity's state as it was when loaded.

StateMeaning
TransientA plain new Product() — Hibernate doesn't know it exists at all.
ManagedLoaded via the EntityManager (or just saved), and tracked in the persistence context — any change to it is noticed at flush time.
DetachedWas managed, but the persistence context it belonged to has closed (e.g. the transaction ended) — changes to it are no longer tracked.
RemovedMarked for deletion — will generate a DELETE at flush time.

◆ Under the hood

The instant productRepository.findById(id) loads the entity, Hibernate takes an internal snapshot of its field values and stores that alongside the actual managed entity object in the persistence context. At flush time (§12.4) — typically right before the transaction commits — Hibernate compares every managed entity's current field values against its stored snapshot, field by field. Any entity where something changed is "dirty", and Hibernate automatically generates and executes an UPDATE statement for exactly the changed columns — without you ever explicitly calling save() or update().

No save() call anywhere — Hibernate compares the entity's current state against its loaded snapshot at flush time and auto-generates the UPDATE for whatever actually changed.

▲ Common mistake

Mutating a detached entity (loaded in one transaction, modified after that transaction/method has ended) does nothing — there's no persistence context left tracking it, so the change is silently lost with no error. This is a very common source of "I updated the object but the database wasn't updated" bugs, and the fix is either re-attaching it via merge() or performing the mutation inside the transaction where it was originally loaded.

◆ The problem

Flush and commit are two different things, easy to conflate — flush is "synchronize the persistence context's pending changes to the database by sending SQL," commit is "make those already-sent changes permanent." Understanding the difference matters because Hibernate can flush before a transaction commits, in specific circumstances.

FlushModeWhen flush happens
AUTO (default)Before commit, and also automatically before any query whose results could be affected by pending unflushed changes.
COMMITOnly right before commit — never triggered by an intermediate query.
MANUALOnly when you explicitly call entityManager.flush() yourself.
@Transactional public void example() { Product product = productRepository.findById(1L).orElseThrow(); product.setPrice(new BigDecimal("999.99")); // under FlushMode.AUTO, Hibernate detects this query overlaps the dirty entity's // table, and automatically flushes the pending UPDATE FIRST — so this query // correctly sees the new price, even though nothing was explicitly saved yet List<Product> expensiveProducts = productRepository.findByPriceGreaterThan(new BigDecimal("500")); }

💻 Code example

@Transactional public void example() { Product product = productRepository.findById(1L).orElseThrow(); product.setPrice(new BigDecimal("999.99")); // under FlushMode.AUTO, Hibernate detects this query overlaps the dirty entity's // table, and automatically flushes the pending UPDATE FIRST — so this query // correctly sees the new price, even though nothing was explicitly saved yet List<Product> expensiveProducts = productRepository.findByPriceGreaterThan(new BigDecimal("500")); }

Want a visual for this concept?

Generate a diagram tailored to “Persistence Context & Dirty Checking” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Entity Lifecycle, Caching & Transaction Boundaries← Back to all Transaction Mastery chapters