Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · staff level

Explain the JPA persistence context. What do dirty checking, flush and the first-level cache mean in a Spring transaction?

🎤 Say this first

The persistence context is the EntityManager's identity map of managed entities for the current transaction: load the same id twice, get the same instance (first-level cache). Managed entities are snapshot-compared at flush — dirty checking — so modifying a loaded entity inside @Transactional persists without calling save(). Flush (writes SQL, doesn't commit) happens before queries that need it, at commit, or manually. In Spring, the context's lifetime is bound to the transaction (or the request, with OSIV) — most 'mysterious' JPA behavior is just not knowing which state an entity is in: transient, managed, detached, removed.

The full picture

Three behaviors that surprise non-JPA people
@Transactional
public void renameCustomer(long id, String name) {
    Customer c = repo.findById(id).orElseThrow();  // managed
    c.setName(name);                               // no save() call…
}                                                   // …flush at commit → UPDATE ✅

@Transactional
public void identity(long id) {
    var a = repo.findById(id).get();
    var b = repo.findById(id).get();
    assert a == b;                                 // same INSTANCE (1st-level cache)
}

public void detached(long id) {                    // NO transaction
    Customer c = repo.findById(id).get();          // context closed after call
    c.setName("x");                                // detached — silently NOT saved
}
  • Flush ordering: JPA flushes pending changes before JPQL queries whose results could be affected (FlushModeType.AUTO) — why a 'read' can suddenly emit UPDATEs in your SQL log.
  • save() semantics (Spring Data): persist for new, merge for detached — merge copies state onto a managed instance and returns it; keeping the old reference is the classic detached-entity bug.
  • Long transactions + big contexts: every managed entity is snapshot-held; loading 100k rows in one TX means dirty-check overhead + memory. Batch jobs need flush()/clear() windows or stateless sessions.
  • Second-level cache is a different animal (shared, cross-transaction, invalidation complexity) — opt-in per entity, justify with read-mostly data.

🔄 Likely follow-up questions

  • getReferenceById vs findById — proxies and when each hits the DB.
  • Why must equals/hashCode be careful for entities in sets before ids are assigned?
  • What does @Transactional(readOnly = true) change in Hibernate specifically?