Orphan removal in Hibernate deletes a child entity from the database when the child is removed from its parent. A child removed this way is called an orphan, because no parent points to it anymore and its row has no use. We turn the feature on with the orphanRemoval = true attribute of @OneToMany or @OneToOne, and Hibernate runs the DELETE for us when the transaction commits.
We use orphan removal for children that cannot exist without their parent, such as the steps of a recipe or the lines of an invoice. For example, when a user deletes a step in a recipe editor, our code removes the step from the list, and the row is gone after the commit.
The following example enables orphan removal on the steps of a recipe and removes steps inside a transaction, with the SQL of each line as a comment.
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Step> steps = new ArrayList<>();
recipe.getSteps().remove(heat); // delete from Step where id=?
recipe.getSteps().clear(); // one delete per step
recipe.setSteps(new ArrayList<>()); // error: never replace the collection
Notice that the last line fails at commit, because Hibernate tracks the collection object it loaded, so we change that collection and never replace it.
Next, we compare orphan removal with CascadeType.REMOVE and run both cases on a recipe app. The FAQs at the end cover replaced collections, @ManyToMany, Spring Data JPA and the timing of the DELETE.
1. What Is Orphan Removal?
A foreign key in the child table links a parent and its children, so removing a child from the parent’s collection only breaks that link. What happens to the child row after that depends on the mapping.
- Without orphanRemoval, Hibernate sets the foreign key to NULL and the row stays in the table.
- With orphanRemoval = true, Hibernate deletes the row.
The CascadeType.REMOVE setting looks similar but reacts to a different action. It deletes the children only when the parent itself is deleted with em.remove().

| Action in code | CascadeType.REMOVE | orphanRemoval = true |
|---|---|---|
| em.remove(recipe) | Steps deleted | Steps deleted |
| recipe.removeStep(heat) | Row kept, recipe_id = NULL | Row deleted |
| recipe.getSteps().clear() | No SQL, rows unchanged | All rows deleted |
| recipe.setNutrition(null) (one-to-one) | Row kept | Row deleted |
With orphanRemoval = true, deleting the parent also deletes the children, even without CascadeType.REMOVE. So the choice depends on one question, namely whether a child can exist without its parent.

2. Orphan Removal Example
The following example is a small recipe app built with Hibernate 7.4, Java 25 and H2. It runs both cases, a child with orphan removal and a child without it.
2.1. Domain Model
A recipe has steps (“Whisk”, “Heat”, “Fry”) and nutrition facts, which both belong to one recipe, so both use orphan removal. A chef has recipes, but a recipe can exist without its chef, so the chef-recipe association has no orphan removal. Tags such as “breakfast” are shared by many recipes.

The parent declares orphanRemoval on the association, and the helper methods keep both sides in sync.
@OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Step> steps = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "nutrition_id")
private Nutrition nutrition;
public void addStep(Step step) {
steps.add(step);
step.setRecipe(this);
}
public void removeStep(Step step) {
steps.remove(step);
step.setRecipe(null);
}
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "recipe_id")
private Recipe recipe;
@OneToMany(mappedBy = "chef")
private List<Recipe> recipes = new ArrayList<>();
public void removeRecipe(Recipe recipe) {
recipes.remove(recipe);
recipe.setChef(null);
}
2.2. Removing a Child Entity
We remove the “Heat” step from the pancake recipe, and the pancake recipe from its chef. Both calls look the same in Java, but each one runs different SQL.
emf.runInTransaction(em -> {
Recipe pancakes = em.find(Recipe.class, recipeId);
pancakes.removeStep(pancakes.findStep("Heat"));
}); // delete from Step where id=?
emf.runInTransaction(em -> {
Chef chef = em.find(Chef.class, chefId);
chef.removeRecipe(em.find(Recipe.class, recipeId));
}); // update Recipe set chef_id=?,name=?,nutrition_id=? where id=?
After the two transactions, the step row is gone, whereas the recipe row stays with chef_id = NULL.

2.3. Orphan Removal in a One-to-One Association
On @OneToOne, the child becomes an orphan when the reference is set to null or replaced. For example, when a user recalculates the nutrition facts of a recipe, we set a new Nutrition object, and Hibernate deletes the old row.
recipe.setNutrition(null); // update Recipe set nutrition_id=? ...
// delete from Nutrition where id=?
recipe.setNutrition(new Nutrition(420)); // insert into Nutrition ...
// update Recipe ...
// delete from Nutrition where id=? (old row)
2.4. Deleting the Parent Entity
The call em.remove(recipe) deletes everything the recipe owns, while the shared tag stays.
delete from recipe_tag where recipe_id=?
delete from Step where id=?
delete from Step where id=?
delete from Recipe where id=?
The complete project on GitHub prints the SQL for each removal case and checks the results with 14 JUnit tests (mvn -q compile exec:java, mvn test).
3. Orphan Removal FAQs
3.1. Why Does Replacing the Collection Throw an Error?
The error comes from assigning a new list to the collection field. Hibernate finds orphans by comparing the collection object it loaded with a snapshot, and a new list is not that object, so Hibernate stops at commit.

// Wrong: replaces the managed collection
recipe.setSteps(newSteps);
// Right: changes the managed collection
recipe.getSteps().clear();
newSteps.forEach(recipe::addStep);
jakarta.persistence.RollbackException: Error while committing the transaction [A collection with orphan deletion was no longer referenced by the owning entity instance: com.howtodoinjava.hibernate.orphan.Recipe.steps]
This error often comes from update code that copies a list from a DTO onto the entity. For example, a recipe edit form sends the new steps in a DTO, and the update code assigns that DTO list to the entity. So we keep the collection without a public setter.
3.2. Does orphanRemoval Work With @ManyToMany?
No. @ManyToMany has no orphanRemoval attribute, because the child is shared. Removing a tag from a recipe deletes only the link row.
pancakes.removeTag(breakfast); // delete from recipe_tag where recipe_id=?
// the Tag row stays
Never put CascadeType.REMOVE or CascadeType.ALL on @ManyToMany, because deleting one recipe would delete tags that other recipes still use.
3.3. Does orphanRemoval Work With Spring Data JPA?
Yes. The method save() calls merge() for an existing entity, and merge() applies the removed child to the managed copy, so the step is deleted. The steps must be loaded before the first transaction closes (for example with join fetch), otherwise reading them throws a LazyInitializationException.
detached.removeStep(detached.findStep("Heat"));
emf.runInTransaction(em -> em.merge(detached)); // delete from Step where id=?
3.4. When Does Hibernate Delete the Orphan?
Hibernate deletes an orphan at flush time, not when our code removes it from the collection. A few more rules decide whether the DELETE runs at all.
| Fact | What it means |
|---|---|
| The DELETE runs at flush | At commit, or earlier when Hibernate flushes before a query, not at the remove() call |
| Works only on managed entities | Load the parent and change it in one transaction |
| clear() keeps the back reference | step.recipe still points to the recipe in memory; use removeStep() in a loop if the objects are used later |
| Moving a child to another parent | Hibernate 7.4 runs an UPDATE, but the JPA specification says portable code must not do it |
| Bulk JPQL delete from Recipe | Skips orphan removal and cascades, and fails on the foreign key |
4. Conclusion
The setting orphanRemoval = true deletes a child as soon as it leaves its parent, whereas CascadeType.REMOVE deletes children only when the parent is deleted.
We use orphan removal for children that cannot exist without their parent. We change the collection inside a transaction and never replace the collection object.
5. References
- Jakarta Persistence 3.2 specification: Entity Relationships
- OneToMany JavaDoc (Jakarta Persistence 3.2)
- OneToOne JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide
Happy Learning !!