What Are the Transient, Persistent, Detached, Removed States
In JPA and Hibernate, every entity instance exists in one of four distinct states relative to the JVM memory and the active PersistenceContext (the Hibernate Session / First-Lev...
🟢 Junior Level
The 4 Fundamental Entity States
In JPA and Hibernate, every entity instance exists in one of four distinct states relative to the JVM memory and the active PersistenceContext (the Hibernate Session / First-Level Cache). An entity’s current state dictates whether modifications are automatically tracked by the ORM, which SQL commands are generated, and when they are dispatched to the relational database:
| Entity State | Bound to PersistenceContext? |
Exists in Database? | Tracked by Dirty Checking? | Description |
|---|---|---|---|---|
Transient (New) |
❌ No | ❌ No | ❌ No | A plain Java object created via new. Unknown to JPA. |
Persistent (Managed) |
✅ Yes | ✅ Yes (or scheduled on flush) | ✅ Yes | Fully managed by the active session. Any field change updates the DB. |
Detached (Unbound) |
❌ No | ✅ Yes | ❌ No | Exists in DB, but its session closed or it was evicted from cache. |
Removed (Deleted) |
✅ Yes (Pending flush) | ⚠️ Exists until flush | ❌ No | Marked for deletion. Executed as SQL DELETE upon flush(). |
JPA ENTITY STATE MACHINE
[ Transient ] (new User())
│
persist() │ merge()
▼
┌────────────────> [ Persistent ] <────────────────┐
│ │ │ │
│ detach()/clear() │ │ remove() │ merge()
│ ▼ ▼ │
│ [ Detached ] [ Removed ] ────────────┘
│ │ │ persist()
└───────────────────┘ ▼
(SQL DELETE on flush)
Code Examples for Each State
// 1. TRANSIENT: Resides solely in JVM heap memory; ID is null
User user = new User();
user.setName("Michael");
// 2. PERSISTENT (Managed): Enrolled in the active persistence context
entityManager.persist(user);
// The user is now managed. Dirty Checking synchronizes modifications without calling save()!
user.setName("Michael Updated");
// 3. DETACHED: Evicted from the persistence context
entityManager.detach(user);
// user retains its ID in memory, but Hibernate no longer monitors it:
user.setName("John"); // This change will NOT be saved to the database!
// 4. PERSISTENT (via merge): Re-attached to management
User managedUser = entityManager.merge(user);
// Note: user remains Detached; managedUser is a new, tracked Managed instance!
// 5. REMOVED: Scheduled for physical deletion
entityManager.remove(managedUser);
// An SQL DELETE is dispatched upon transaction commit or entityManager.flush()
Programmatic State Inspection
// 1. Verify if an instance is currently MANAGED (Persistent)
boolean isManaged = entityManager.contains(user);
// 2. Diagnostic state heuristic (for auto/sequence-generated IDs):
if (user.getId() == null) {
log.info("Entity is TRANSIENT (New in memory)");
} else if (entityManager.contains(user)) {
log.info("Entity is PERSISTENT / MANAGED");
} else {
log.info("Entity is DETACHED (Has DB ID, but untracked by active session)");
}
🟡 Middle Level
Operation and State Transition Matrix
| Initial State | EntityManager Method |
Resulting State | Database Action & Behavior |
|---|---|---|---|
Transient |
persist(e) |
Persistent |
Schedules SQL INSERT (executes immediately with IDENTITY). |
Transient |
merge(e) |
Persistent (Copy) |
Creates new copy, schedules INSERT, returns managed copy. |
Persistent |
detach(e) / evict(e) |
Detached |
Evicts instance from L1 cache. Zero SQL emitted. |
Persistent |
remove(e) |
Removed |
Marks entity for deletion. Dispatches DELETE on flush. |
Persistent |
refresh(e) |
Persistent |
Executes SELECT, overwriting in-memory modifications. |
Detached |
merge(e) |
Persistent (Copy) |
Executes SELECT, copies fields into managed clone, schedules UPDATE. |
Detached |
persist(e) |
❌ Exception | Throws EntityExistsException or PersistentObjectException. |
Detached |
remove(e) |
❌ Exception | Throws IllegalArgumentException: Removing a detached instance. |
Removed |
persist(e) |
Persistent |
Cancels deletion, restoring instance to Managed status. |
The Critical merge() Invariant: Retaining Detached References
A ubiquitous source of silent production bugs is ignoring the return value of merge():
// Entity retrieved in a prior transaction or deserialized from a REST request
User detachedUser = userService.findDetached(1L);
detachedUser.setName("New Name");
// BUG: Return value ignored!
entityManager.merge(detachedUser);
detachedUser.setEmail("new@example.com");
// detachedUser remains in the Detached state! email is NEVER persisted to the database!
Anatomy of merge():
- Hibernate checks if an entity with this primary key is already resident in the active
PersistenceContext. - If absent, it loads the row from the database via an SQL
SELECT(or instantiates a new entity if non-existent). - It copies all field values from the passed
detachedUserinto the loaded/createdManagedentity. - It returns the reference to that managed clone.
- The original argument (
detachedUser) remains in theDetachedstate.
// CORRECT PATTERN:
User managedUser = entityManager.merge(detachedUser);
managedUser.setEmail("new@example.com"); // Persisted via Dirty Checking!
Cascading State Transitions (CascadeType)
The JPA cascade configuration propagates state transitions across parent-child association graphs:
CascadeType.PERSIST: Passing parent topersist()cascadespersist()to associated children.CascadeType.MERGE: Callingmerge()on the parent cascadesmerge()across associated entities.CascadeType.REMOVE: Transitioning the parent toRemovedtransitions associated children toRemoved.CascadeType.DETACH: Detaching the parent (detach()) evicts associated child entities from the L1 cache.
🔴 Senior Level
Under the Hood: StatefulPersistenceContext and EntityEntry
Inside Hibernate’s SessionImpl, the persistence context is implemented via StatefulPersistenceContext. It maintains two vital internal maps:
entitiesByKey: Map<EntityKey, Object>— The Identity Map (First-Level Cache). Guarantees the fundamental JPA invariant: within a single session, one database row maps to exactly one Java object reference.entityEntries: IdentityMap<Object, EntityEntry>— The state and snapshot tracking registry.
StatefulPersistenceContext
├── entitiesByKey: [ EntityKey(User#1) -> User@0x4a12 ]
└── entityEntries:
└── User@0x4a12 -> EntityEntry {
status: Status.MANAGED,
loadedState: ["Michael", "misha@mail.com", 30], // Initial snapshot array
id: 1,
version: 0,
existsInDatabase: true
}
Lifecycle of EntityEntry:
Status.MANAGED: The object is indexed inentitiesByKey, and itsloadedStatearray contains original column values.- On
detach()orclear(): The entity is removed from bothentitiesByKeyandentityEntries. TheloadedStatesnapshot array is garbage collected. - On
remove(): The status inEntityEntryflips toStatus.DELETED. The object remains in context untilflush(). - On
flush(): Hibernate iterates throughentityEntries, compares the current object fields withloadedState(Dirty Checking), and enqueues SQL operations intoActionQueue.
The Assigned ID Dilemma (Transient vs. Detached)
When entities use manually assigned primary keys without @GeneratedValue (Assigned ID):
@Entity
public class Payment {
@Id
private String transactionUuid; // Manually assigned identifier
private BigDecimal amount;
}
Hibernate cannot rely on id == null to determine whether an instance is Transient (new) or Detached (existing).
Upon invoking merge() or saveOrUpdate(), Hibernate is forced to execute an exploratory SQL SELECT to verify row existence:
- Executes
SELECT transaction_uuid FROM payment WHERE transaction_uuid = ?. - If found $\rightarrow$ entity is treated as
Detached(issues SQLUPDATE). - If not found $\rightarrow$ entity is treated as
Transient(issues SQLINSERT).
Senior Architectural Remedy:
Implement Spring Data’s Persistable<ID> interface (providing a custom isNew() method), or declare an optimistic locking @Version property. If @Version == null, Hibernate knows the entity is definitively Transient, completely eliminating the penalty SELECT query.
High-Volume Batch Processing and L1 Cache Memory Leaks
Because all Persistent instances are held by strong references within StatefulPersistenceContext.entityEntries, processing massive datasets quickly causes an OutOfMemoryError:
@Transactional
public void processLargeDataset() {
int batchSize = 50;
for (int i = 0; i < 100_000; i++) {
User user = new User("User_" + i);
entityManager.persist(user);
if (i > 0 && i % batchSize == 0) {
entityManager.flush(); // Sends accumulated INSERTs to database
entityManager.clear(); // Transitions all entities to DETACHED, freeing heap
}
}
}
Alternative: Use Hibernate’s StatelessSession, a command-oriented abstraction that completely bypasses the First-Level Cache, Dirty Checking, and Entity Lifecycle state machines for raw JDBC throughput with zero heap accumulation.
4 Tricky Questions
1. How does Hibernate distinguish between Transient and Detached when calling merge() if the entity uses an assigned ID and has no @Version field?
Answer:
- When
@GeneratedValueis used, the check is trivial:id == nullindicatesTransient;id != nullindicatesDetached. - With manually assigned keys (
Assigned ID) and no@Versionfield, Hibernate executes an exploratory SQLSELECTagainst the database using the entity’s primary key. - If the query returns a record, Hibernate classifies the entity as
Detachedand executes an SQLUPDATE. - If the query returns empty, Hibernate classifies the entity as
Transientand executes an SQLINSERT. - To avoid this penalty
SELECT, the entity should implementorg.springframework.data.domain.Persistable<ID>with a customisNew()implementation, or include an@Versionproperty.
2. What happens if you modify a Detached entity, invoke entityManager.merge(detached), and subsequently continue modifying the original Detached reference?
Answer:
- Modifications made to the
detachedobject prior to callingmerge()are copied into the returnedManagedcopy and successfully synchronized to the database. - Modifications made to the original
detachedreference after callingmerge()are lost, because that reference remains detached and untracked. - Only modifications made directly to the returned managed reference (
User managed = entityManager.merge(detached); managed.setField(...)) are tracked by Dirty Checking and persisted to the database.
3. Can an entity in the Removed state be restored to Persistent? What happens internally?
Answer:
- Yes, the JPA specification explicitly allows calling
entityManager.persist(removedEntity)provided thatflush()has not yet executed. - Internally, the status in
EntityEntrychanges fromStatus.DELETEDback toStatus.MANAGED. - The scheduled SQL
DELETEoperation is removed from Hibernate’sActionQueue. - If
flush()has already executed, the SQLDELETEhas already been dispatched to the database socket, and subsequent invocations risk primary key violations or re-insertions depending on key generation strategies.
4. Why does calling entityManager.lock(detachedEntity, LockModeType.OPTIMISTIC) throw an exception while merge() succeeds?
Answer:
- Concurrency locks (both optimistic and pessimistic) can be acquired exclusively on entities in the
Managedstate residing inside the activePersistenceContext. - Calling
lock()on aDetachedobject immediately throwsIllegalArgumentException: entity not managed. - Conversely,
merge()is explicitly designed to accept detached objects; it loads a managed copy into the persistence context. To lock a detached object, you must first callUser managed = entityManager.merge(detachedUser)and passmanagedtoentityManager.lock().
🎯 Interview Cheat Sheet
30-Second Elevator Pitch
“JPA entities transition across four fundamental states:
Transient(new POJO, no ID, not in session),Persistent(enrolled inPersistenceContext/ L1 cache, primary key assigned, automatically tracked via Dirty Checking),Detached(session closed or evicted viadetach()/clear(), modifications untracked), andRemoved(scheduled for SQLDELETEupon flush). The primary mechanism for returning a detached entity to persistent status ismerge(), which loads and returns a managed copy while leaving the input argument detached. In high-volume batch processing, developers must regularly callflush()andclear()to transition entities into theDetachedstate to avoid heap memory exhaustion.”
Key Production Monitoring Metrics
- Hibernate Session Entity Count (
Session.getStatistics().getEntityCount()): Detects memory leaks caused by missingclear()calls during batch loops. - Flush Phase Duration: Measures the time Dirty Checking spends traversing
entityEntries. - Pre-Merge SELECT Queries: Tracks unnecessary exploratory SQL queries caused by assigned IDs lacking
isNew()or@Version.
Red Flags (DO NOT Say)
- ❌ “Calling
merge(entity)mutates the passed instance into a managed entity.” (The passed instance remains detached;merge()returns a separate managed clone). - ❌ “A detached entity can be deleted directly using
em.remove(detached).” (ThrowsIllegalArgumentException; it must be managed first). - ❌ “Entities in the Transient state already exist in the database.” (Transient entities exist only in JVM heap memory).
- ❌ “Dirty checking monitors field changes on Detached entities.” (Modifications to detached entities are completely ignored until merged).
Related Topics
- Describe the Entity Lifecycle in Hibernate — Lifecycle overview and state transitions
- What is Dirty Checking in Hibernate — Snapshot dirty checking
- How Does the Flush Mechanism Work in Hibernate — Flush order and ActionQueue
- What is the Difference Between persist() and merge() — Persist vs merge
- What is the First-Level Cache in Hibernate — Persistence context and L1 cache