Fixing org.hibernate.PersistentObjectException: detached entity passed to persist in Spring Data JPA

intermediate Java2026-07-25| Java 11+, Spring Boot 2.x/3.x, Hibernate 5/6, PostgreSQL/MySQL

Error Message

org.hibernate.PersistentObjectException: detached entity passed to persist
#java#spring-boot#hibernate#jpa

The Error Message

It usually happens when you least expect it. You call a save method, and your console explodes with a stack trace like this:

org.hibernate.PersistentObjectException: detached entity passed to persist
    at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:124)
    at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:775)
    at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:754)

Why Hibernate is Complaining

This error is Hibernate's way of saying: "You asked me to create a brand-new record, but this object already has an ID that belongs to an existing one." Hibernate tracks entities using a state machine. To fix this, you need to know which state your object is in.

- **Transient:** A fresh object. It has no ID and Hibernate doesn't know it exists yet.
- **Persistent:** An object tied to the current database session. Hibernate tracks every change you make to it.
- **Detached:** An object that has an ID (like `id=101`) but is no longer linked to an active session.

The persist() method is picky. It only accepts transient entities. If you pass a detached entity—one that already has a primary key—Hibernate panics because it doesn't know whether to update the old record or ignore the ID.

Common Scenarios and Fixes

1. The Manual ID Trap

Are you manually setting the ID on a new object? If you assign entity.setId(500L) and then call repository.save(), Hibernate sees that ID and assumes the record already exists. When it tries to run a persist operation on what it thinks is a new record, the conflict triggers the exception.

The Fix: Let the database handle the heavy lifting. Use @GeneratedValue and don't touch the ID field for new entries. If you must use manual IDs, ensure your entity implements Persistable so you can explicitly tell Spring Data JPA whether the object is actually new.

2. Cascade Pitfalls (The #1 Culprit)

Most developers run into this during Parent-Child operations. Imagine you fetch an existing Category (ID: 5) from the database and attach it to a brand-new Product. If your mapping looks like this:

@ManyToOne(cascade = CascadeType.PERSIST)
private Category category;

When you save the Product, Hibernate tries to persist the Category too. But since the Category already has ID 5, the PERSIST operation fails. It sees a detached entity where it expected a new one.

The Fix: Update your cascade logic. Use MERGE or ALL so Hibernate knows how to handle existing records.

// Instead of PERSIST only, use both:
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private Category category;

3. Handling JSON Data from REST APIs

When a mobile app or frontend sends a JSON object containing an ID, Hibernate treats it as detached. If your service layer tries to save this object directly using entityManager.persist(), it will fail every time.

The Fix: Use repository.save(). This Spring Data JPA method is smart. It checks if the ID exists; if it does, it calls merge(). If the ID is null, it calls persist(). If you are working with the EntityManager directly, always prefer em.merge(entity) for objects coming from outside the system.

4. Out-of-Sync Database Sequences

Sometimes the code is fine, but the data is messy. If your PostgreSQL sequence starts at 1, but you manually inserted 100 rows, Hibernate might try to generate ID 1. Since ID 1 already exists in the table, the persistence context gets confused.

The Fix: Synchronize your sequences. Run a SQL command like SELECT setval('your_sequence_name', (SELECT max(id) FROM your_table)); to ensure Hibernate generates IDs that aren't already taken.

Testing the Fix

Don't guess—verify. You can write a quick integration test to simulate saving a new parent with an existing child. This ensures your CascadeType settings are actually working.

@SpringBootTest
class SaveOperationTest {
    @Autowired private ProductRepository productRepo;
    @Autowired private CategoryRepository categoryRepo;

    @Test
    void shouldSaveProductWithExistingCategory() {
        Category existing = categoryRepo.save(new Category("Electronics"));
        
        Product laptop = new Product("MacBook");
        laptop.setCategory(existing);

        // This will throw PersistentObjectException if cascade is only PERSIST
        assertDoesNotThrow(() -> productRepo.save(laptop));
    }
}

Best Practices for Clean Persistence

- **Stick to Repositories:** Avoid mixing `EntityManager` and `JpaRepository` in the same service. Repositories handle the `persist` vs `merge` logic automatically.
- **Audit your Cascades:** `CascadeType.ALL` is convenient but dangerous. Use specific types like `{PERSIST, MERGE}` to avoid accidental deletions or state conflicts.
- **Use DTOs:** Never pass your raw Entities directly to the frontend. Map your DTOs to a fresh Entity fetched from the database to keep the Hibernate session clean.

Related Error Notes