JPA
Hibernate
PersistentObjectException
Java programming
Database management

PersistentObjectException detached entity passed to persist thrown by JPA and Hibernate

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

The PersistentObjectException: detached entity passed to persist error means you called EntityManager.persist() on an entity that already has a database identity (a non-null @Id). The fix is to use merge() instead of persist() for detached entities, or to ensure the entity is truly new before persisting it.

Understanding Entity Lifecycle States

Every JPA entity exists in one of four states. The PersistentObjectException happens when you confuse two of them.

StateHas @Id?Managed by EntityManager?In Database?
New / TransientNo (or null)NoNo
Managed / PersistentYesYesYes
DetachedYesNoYes
RemovedYesYes (scheduled for delete)Yes (until flush)

The key distinction: persist() expects a new entity with no database identity. If the entity has an @Id value and is not currently managed, JPA considers it detached and throws PersistentObjectException.

java
1// This works: entity is new (transient)
2User user = new User();
3user.setName("Alice");
4entityManager.persist(user); // ID is null, so JPA assigns one
5
6// This throws PersistentObjectException: entity is detached
7User user = new User();
8user.setId(42L); // manually setting an ID makes it "detached"
9entityManager.persist(user); // ERROR: detached entity passed to persist

The Five Most Common Causes

1. Persisting an Entity Fetched from a Closed Session

This is the most frequent cause. You fetch an entity, close the EntityManager (or the transaction ends), then try to persist the same object in a new context:

java
1// First transaction
2User user;
3EntityManager em1 = emf.createEntityManager();
4em1.getTransaction().begin();
5user = em1.find(User.class, 1L);
6em1.getTransaction().commit();
7em1.close(); // user is now DETACHED
8
9// Second transaction
10EntityManager em2 = emf.createEntityManager();
11em2.getTransaction().begin();
12user.setEmail("[email protected]");
13em2.persist(user); // ERROR: detached entity passed to persist
14em2.getTransaction().commit();

Fix: Use merge() instead of persist():

java
em2.merge(user); // This works for detached entities

2. Parent-Child Cascade with a Detached Parent

When you use CascadeType.PERSIST or CascadeType.ALL on a relationship, persisting the child also cascades persist() to the parent. If the parent is detached, the cascade fails:

java
1@Entity
2public class Order {
3    @Id @GeneratedValue
4    private Long id;
5
6    @ManyToOne(cascade = CascadeType.ALL)
7    private Customer customer;
8}
9
10// customer was loaded in a previous session, now detached
11Customer customer = getExistingCustomer(); // detached entity
12
13Order order = new Order();
14order.setCustomer(customer); // associates detached customer
15
16entityManager.persist(order); // ERROR: cascades persist to detached customer

Fix: Either merge the parent first, or change the cascade type:

java
1// Option 1: merge the parent before persisting the child
2Customer managedCustomer = entityManager.merge(customer);
3order.setCustomer(managedCustomer);
4entityManager.persist(order);
5
6// Option 2: remove CascadeType.PERSIST from the relationship
7@ManyToOne // no cascade
8private Customer customer;

3. Using GenerationType.AUTO with a Pre-Set ID

If your entity uses @GeneratedValue but you manually set the ID before persisting, Hibernate treats it as detached:

java
1@Entity
2public class Product {
3    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
4    private Long id;
5}
6
7Product product = new Product();
8product.setId(100L); // Don't do this with @GeneratedValue
9entityManager.persist(product); // ERROR or unexpected behavior

Fix: Do not set the @Id field when using @GeneratedValue. Let Hibernate assign it.

4. Detached Entities from DTOs or Deserialization

When you receive an entity from a REST API request, JSON deserialization creates a new Java object with a populated ID. This object is not managed by any persistence context:

java
1@PostMapping("/users")
2public void updateUser(@RequestBody User user) {
3    // user has an ID from the JSON body but is not managed
4    entityManager.persist(user); // ERROR: detached entity
5}

Fix: Use merge() or find-then-update:

java
1// Option 1: merge
2entityManager.merge(user);
3
4// Option 2: find-then-update (more explicit, better control)
5User managed = entityManager.find(User.class, user.getId());
6managed.setName(user.getName());
7managed.setEmail(user.getEmail());
8// no persist needed, dirty checking handles the update

5. Bidirectional Relationships with Inconsistent State

With bidirectional @OneToMany / @ManyToOne mappings, failing to synchronize both sides can cause detached references:

java
1@Entity
2public class Department {
3    @OneToMany(mappedBy = "department", cascade = CascadeType.ALL)
4    private List<Employee> employees = new ArrayList<>();
5}
6
7@Entity
8public class Employee {
9    @ManyToOne
10    private Department department;
11}
12
13// Wrong: only setting one side
14Department dept = entityManager.find(Department.class, 1L);
15Employee emp = new Employee();
16emp.setDepartment(dept);
17// dept.getEmployees() still doesn't contain emp
18entityManager.persist(emp); // may cause issues with cascade

Fix: Always synchronize both sides of a bidirectional relationship:

java
1Department dept = entityManager.find(Department.class, 1L);
2Employee emp = new Employee();
3emp.setDepartment(dept);
4dept.getEmployees().add(emp); // synchronize both sides
5entityManager.persist(emp);

persist() vs merge(): When to Use Each

MethodInput StateWhat It DoesReturns
persist()Transient (new)Inserts a new row, assigns IDvoid (original object becomes managed)
merge()Detached or transientCopies state into a managed entity, inserts or updatesManaged copy (original stays detached)

Critical difference in merge() behavior: it returns a new managed instance. The original object remains detached:

java
1User detached = new User();
2detached.setId(42L);
3detached.setName("Bob");
4
5User managed = entityManager.merge(detached);
6
7// detached != managed (different object references)
8detached.setName("Alice"); // this change is NOT tracked
9managed.setName("Alice");  // this change IS tracked and will be flushed

Spring Data JPA: save() Handles This Automatically

If you use Spring Data JPA, the save() method internally checks whether to call persist() or merge():

java
1@Repository
2public interface UserRepository extends JpaRepository<User, Long> {}
3
4// Spring calls persist() for new entities, merge() for existing ones
5userRepository.save(user); // works for both new and detached entities

Spring determines "new vs existing" by checking if the @Id is null (for generated IDs) or by implementing Persistable<ID> for assigned IDs:

java
1@Entity
2public class User implements Persistable<Long> {
3    @Id
4    private Long id;
5
6    @Transient
7    private boolean isNew = true;
8
9    @Override
10    public boolean isNew() {
11        return isNew;
12    }
13
14    @PostLoad
15    @PostPersist
16    void markNotNew() {
17        this.isNew = false;
18    }
19}

Common Pitfalls

  • Using merge() everywhere "just to be safe": merge() always issues a SELECT before the INSERT/UPDATE to check for existing records. For batch inserts of new entities, persist() is significantly faster because it skips this check.
  • Ignoring the return value of merge(): The object you passed to merge() is still detached. Always use the returned managed instance for further operations.
  • Using CascadeType.ALL on @ManyToOne: This cascades persist() to the parent entity, which is almost always already persisted. Use CascadeType.ALL on the @OneToMany side and be selective on @ManyToOne.
  • Confusing persist() with save() semantics: JPA has persist() and merge(). Spring Data JPA's save() is a convenience method that calls one or the other. If you are using raw JPA (no Spring), you must choose the right method yourself.
  • Forgetting @GeneratedValue: Without this annotation, JPA does not generate IDs automatically. An entity with a null @Id and no @GeneratedValue may be treated differently depending on the provider.

Summary

  • PersistentObjectException means persist() received an entity with an existing database identity (non-null @Id).
  • Use persist() only for new entities with no ID. Use merge() for detached entities that need reattaching.
  • The most common trigger is cascading persist() to a detached parent via CascadeType.ALL or CascadeType.PERSIST.
  • merge() returns a new managed instance. Always use the returned object, not the original.
  • Spring Data JPA's save() method handles the persist() vs merge() decision automatically.
  • For deserialized entities from REST APIs, prefer find-then-update over merge() for explicit control over what gets modified.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.