Entity Lifecycle States: Transient, Managed, Detached, Removed
Introduction
Every JPA entity is always in one of four states. The state determines whether Hibernate is tracking the entity, whether changes are detected automatically, and what operations are valid. Understanding these states explains a large class of JPA bugs — especially the dreaded LazyInitializationException and detached entity errors.
The Four States
new Customer()
│
│ persist / save()
▼
TRANSIENT ──────────────────────► MANAGED
(not tracked) (tracked by persistence context)
│
│ evict / clear / close
▼
DETACHED
(no longer tracked)
│
│ merge()
▼
MANAGED (re-attached)
MANAGED ─── remove / delete() ──► REMOVED
(delete scheduled, still in context)
1. Transient
An entity is Transient when it has been created with new but has not yet been associated with a persistence context. JPA does not know about it — no tracking, no SQL, no identity.
// Transient — just a regular Java object
Customer customer = new Customer();
customer.setName("Alice");
customer.setEmail("alice@example.com");
// At this point:
// - No id assigned
// - Not in any persistence context
// - No SQL generated
// - Changes are NOT tracked
A transient entity has id = null (or 0 for primitives) if @GeneratedValue is configured — the database has not assigned an id yet.
Transition: Transient → Managed
Call save() (Spring Data JPA) or em.persist() (raw JPA). The entity enters the persistence context and an INSERT is scheduled.
@Transactional
public Customer createCustomer(String name, String email) {
Customer customer = new Customer(); // TRANSIENT
customer.setName(name);
customer.setEmail(email);
customer.setCreatedAt(LocalDateTime.now());
Customer saved = customerRepository.save(customer); // now MANAGED
// saved.getId() is assigned — Hibernate executed INSERT and populated the id
return saved;
}
2. Managed (Persistent)
A Managed entity is inside the persistence context. Hibernate tracks it:
- Its state is compared against a snapshot at flush time
- Changes are automatically written to the database (dirty checking)
- It has a database identity (
idis not null)
@Transactional
public void updateEmail(Long id, String newEmail) {
// findById → entity enters persistence context → MANAGED
Customer customer = customerRepository.findById(id).orElseThrow();
// Modify the managed entity
customer.setEmail(newEmail);
// No save() call needed — Hibernate detects the change
// At commit: UPDATE customers SET email=? WHERE id=?
}
What makes an entity managed?
| Operation | Result |
|---|---|
repository.save(transientEntity) | Calls em.persist() → Managed |
repository.findById(id) | Loads from DB → Managed |
repository.findAll() | Loads list → all Managed |
em.merge(detachedEntity) | Merges → returns new Managed copy |
The managed identity rule
Within one persistence context, an entity with a given (type, id) pair has exactly one managed instance:
@Transactional
public void identityDemo(Long id) {
Customer a = customerRepository.findById(id).orElseThrow();
Customer b = customerRepository.findById(id).orElseThrow(); // cache hit
assert a == b; // true — same object reference
a.setName("Modified");
System.out.println(b.getName()); // "Modified" — same object
}
3. Detached
A Detached entity has a database identity (has an id) but is no longer associated with a persistence context. Changes to a detached entity are not tracked.
Detachment happens when:
- The transaction (and thus the persistence context) ends
em.detach(entity)is called explicitlyem.clear()is called (detaches all entities)- The
EntityManageris closed
@Transactional
public Customer loadCustomer(Long id) {
return customerRepository.findById(id).orElseThrow();
// transaction ends here → persistence context CLOSES → entity DETACHED
}
public void callerMethod() {
Customer customer = loadCustomer(1L);
// customer is now DETACHED
customer.setEmail("new@example.com");
// This change is NOT tracked — no SQL will be generated
// unless you explicitly merge the entity back
}
The Detached trap: modifying and expecting an update
This is a common bug:
@Service
public class CustomerService {
public Customer getCustomer(Long id) {
// @Transactional NOT present — but repository has its own transaction
return customerRepository.findById(id).orElseThrow();
// entity is DETACHED after this method returns
}
public void updateCustomer(Long id, String email) {
Customer customer = getCustomer(id); // DETACHED
customer.setEmail(email); // change not tracked
// THIS DOES NOT SAVE THE CHANGE — you must call save() or merge()
// customerRepository.save(customer) would work here
}
}
Transition: Detached → Managed (re-attaching)
Call repository.save(entity) or em.merge(entity):
public void mergeDetached(Customer detachedCustomer) {
// save() calls em.merge() internally when entity has an id
Customer managed = customerRepository.save(detachedCustomer);
// managed is the newly returned MANAGED instance
// detachedCustomer itself remains detached
}
Important: merge() returns a new managed instance. The original detached object is not re-attached — it remains detached. Always use the returned instance.
Customer detached = loadCustomer(1L);
detached.setEmail("new@example.com");
Customer managed = customerRepository.save(detached);
// managed is now tracked — changes to managed will be persisted
// detached is still detached — changes to detached will be ignored
4. Removed
A Removed entity has been scheduled for deletion. It is still inside the persistence context but marked for removal. The DELETE SQL runs at flush time.
@Transactional
public void deleteCustomer(Long id) {
Customer customer = customerRepository.findById(id).orElseThrow();
// customer is MANAGED
customerRepository.delete(customer);
// customer is now REMOVED — DELETE scheduled
// At commit: DELETE FROM customers WHERE id=?
}
A removed entity still exists in the persistence context until the transaction commits. Calling em.persist(removedEntity) can cancel the removal and make it managed again.
State Transitions: Complete Map
new Entity()
│ new / transient
│
▼
TRANSIENT
│
│ persist() / save()
▼
MANAGED ◄──────────────── merge(detachedEntity)
│
├── evict() / clear() / context closes ──► DETACHED
│
├── remove() / delete() ──► REMOVED
│ │
│ │ persist(removedEntity) (cancel remove)
│ ▼
│ MANAGED
│
└── transaction commit / flush
│
▼
SQL executed (INSERT / UPDATE / DELETE)
LazyInitializationException — A Detached Entity Problem
The most common detached entity error is LazyInitializationException:
org.hibernate.LazyInitializationException:
failed to lazily initialize a collection of role: Customer.orders:
could not initialize proxy - no Session
This happens when you access a lazy-loaded relationship on a detached entity:
@Transactional
public Customer loadCustomer(Long id) {
return customerRepository.findById(id).orElseThrow();
// transaction ends → entity DETACHED → orders collection NOT loaded
}
public void callerMethod() {
Customer customer = loadCustomer(1L);
// LazyInitializationException! The persistence context is closed.
// Hibernate cannot load the orders collection from a detached entity.
List<Order> orders = customer.getOrders();
}
Solutions (covered in depth in the Fetching articles):
- Load the relationship inside the transaction before returning
- Use
@EntityGraphto eagerly fetch required associations - Use DTO projections that fetch only what is needed
- Use
JOIN FETCHin the query
Practical Examples: Recognising the State
@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository customerRepository;
@Transactional
public void demonstrateStates() {
// ── TRANSIENT ────────────────────────────────────
Customer c = new Customer();
c.setName("Alice");
// c.id == null, not tracked
// ── MANAGED ──────────────────────────────────────
c = customerRepository.save(c); // persist → MANAGED
// c.id != null, tracked, changes auto-synced to DB
c.setEmail("alice@example.com"); // dirty → UPDATE scheduled
// ── REMOVED ──────────────────────────────────────
customerRepository.delete(c); // REMOVED
// DELETE scheduled
} // transaction commits → UPDATE and DELETE flushed
@Transactional
public Customer load(Long id) {
return customerRepository.findById(id).orElseThrow();
// returns MANAGED
} // transaction ends → returned entity becomes DETACHED
public void outsideTransaction() {
Customer detached = load(1L); // DETACHED after load() returns
detached.setName("Bob"); // not tracked — no SQL
customerRepository.save(detached); // merge → re-attached → UPDATE
}
}
equals() and hashCode() with Detached Entities
Because JPA’s identity guarantee only holds within one persistence context, equals/hashCode on entities needs care. The safest approach:
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Use business key (natural key) for equals/hashCode, not the surrogate id
// Or use id with null-safe comparison:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Customer)) return false;
Customer other = (Customer) o;
return id != null && id.equals(other.id);
}
@Override
public int hashCode() {
// Constant hashCode so entity can be placed in Set before and after persist
return getClass().hashCode();
}
}
Using a natural/business key (email for Customer) is even safer — the key is stable across transient, managed, detached states:
@Override
public boolean equals(Object o) {
if (!(o instanceof Customer)) return false;
Customer other = (Customer) o;
return email != null && email.equals(other.email);
}
@Override
public int hashCode() {
return email == null ? 0 : email.hashCode();
}
Key Takeaways
- Transient: new object, not known to JPA, no id, no tracking
- Managed: inside the persistence context, changes auto-detected, id assigned
- Detached: has an id but not in any persistence context — changes are NOT tracked
- Removed: deletion scheduled, still in context until commit
save()on a transient entity =persist()→ Managed;save()on a detached entity =merge()→ returns a new Managed copyLazyInitializationExceptionalways means accessing a lazy relationship on a detached entity — the persistence context is already closedmerge()returns a new managed instance; the original detached object remains detached
What’s Next
Article 5 starts Part 2: Entity Mappings — covering @Entity, @Table, @Id, and @Column with all their attributes and how Hibernate translates them into database schema.