Spring BootCascade Types
Spring Boot

Cascade Types

Cascade types control which JPA lifecycle operations — persist, merge, remove, refresh, detach — are automatically propagated from a parent entity to its associated child entities. Choosing the wrong cascade type is one of the most common sources of accidental data loss and unexpected database operations in Spring Boot applications. Each type has a precise meaning and a correct use case.

The Six Cascade Types

JPA defines six cascade types, each corresponding to one EntityManager operation. CascadeType.ALL is a shortcut for all six. Cascade types are declared on the relationship annotation — @OneToMany, @OneToOne, @ManyToOne, or @ManyToMany — and apply in the direction from the entity that declares the cascade to the associated entity.
Java
// ── The six JPA cascade types: ───────────────────────────────────────
CascadeType.PERSIST   // em.persist(parent) → em.persist(child)
CascadeType.MERGE     // em.merge(parent)   → em.merge(child)
CascadeType.REMOVE    // em.remove(parent)  → em.remove(child)
CascadeType.REFRESH   // em.refresh(parent) → em.refresh(child)
CascadeType.DETACH    // em.detach(parent)  → em.detach(child)
CascadeType.ALL       // all five above combined

// ── Declared on the relationship annotation: ──────────────────────────
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> items;

@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinColumn(name = "address_id")
private Address billingAddress;

@ManyToOne(cascade = CascadeType.MERGE)   // rare — see ManyToOne section
@JoinColumn(name = "category_id")
private Category category;

// ── Cascade direction: ────────────────────────────────────────────────
// Cascade flows FROM the entity that declares it TO the associated entity.
// Order declares cascade → OrderItem receives cascaded operations.
// OrderItem does NOT cascade back to Order (unless declared there too).

// ── Spring Data JPA — save() and delete() map to EntityManager ops: ──
// repository.save(parent)   → em.persist(parent) or em.merge(parent)
// repository.delete(parent) → em.remove(parent)
// Cascade applies to both paths.

CascadeType.PERSIST

CascadeType.PERSIST propagates em.persist() from parent to child. When you save a new parent that holds new (transient) child instances, the children are automatically persisted without explicit calls to save each one.
Java
@Entity
public class Order {

    @OneToMany(mappedBy = "order",
               cascade = CascadeType.PERSIST,   // persist children with parent
               fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();

    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
    }
}

// ── Effect: new children saved automatically with the parent ──────────
Order order = new Order("REF-001");
order.addItem(new OrderItem("Widget", 2, new BigDecimal("9.99")));
order.addItem(new OrderItem("Gadget", 1, new BigDecimal("49.99")));

orderRepository.save(order);
// INSERT INTO orders ...            (1 query)
// INSERT INTO order_items ...       (1 query per item — 2 queries)
// Without CascadeType.PERSIST, only the order would be inserted;
// the items would remain transient and Hibernate would throw
// TransientPropertyValueException.

// ── IMPORTANT: only applies to NEW (transient) children ──────────────
// Existing (already persisted) children do not need PERSIST cascade.
// If a child already has an ID, Hibernate treats it as managed/detached,
// not transient — PERSIST does not re-insert it.

// ── Merge behaviour without MERGE cascade: ────────────────────────────
// CascadeType.PERSIST alone does NOT cascade merge.
// If you load an order (detached), modify its items, and call save(),
// the items will NOT be merged unless CascadeType.MERGE is also declared.
// Usually you want both: cascade = {CascadeType.PERSIST, CascadeType.MERGE}
// or simply: cascade = CascadeType.ALL

CascadeType.MERGE

CascadeType.MERGE propagates em.merge() from parent to child. When you merge a detached parent (e.g. after a web request round-trip), the children are also merged — their state is synchronised with the persistence context.
Java
@Entity
public class Post {

    @OneToMany(mappedBy = "post",
               cascade = {CascadeType.PERSIST, CascadeType.MERGE},
               fetch = FetchType.LAZY)
    private List<Comment> comments = new ArrayList<>();
}

// ── When MERGE cascade matters: ───────────────────────────────────────
// Scenario: load post, pass to a form, receive modified post from the request,
// then save it back. The Post object returned from the form is DETACHED.

// Load (managed inside transaction):
Post post = postRepository.findByIdWithComments(1L).orElseThrow();
// Transaction ends — post is now DETACHED

// Modify outside transaction (e.g. a DTO is applied to the entity):
post.getComments().get(0).setBody("Updated comment body");

// Save detached entity — triggers em.merge(post):
postRepository.save(post);
// WITH CascadeType.MERGE:    → em.merge(comment) called for each comment
//                            → UPDATE comments SET body = ? WHERE id = ?
// WITHOUT CascadeType.MERGE: → comments are NOT merged
//                            → Hibernate may throw DetachedObjectException
//                            → or silently ignore comment changes

// ── CascadeType.MERGE on @ManyToOne — uncommon but valid: ─────────────
// If Product is independent (managed elsewhere), do NOT cascade merge:
@ManyToOne(fetch = FetchType.LAZY)   // no cascade — product is independent
@JoinColumn(name = "product_id")
private Product product;

// If Address is owned by the parent (created/deleted with it), cascade merge:
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE},
           fetch = FetchType.LAZY)
@JoinColumn(name = "billing_address_id")
private Address billingAddress;

CascadeType.REMOVE

CascadeType.REMOVE propagates em.remove() from parent to child. When the parent is deleted, the children are deleted too. This is appropriate for owned children (OrderItem owned by Order) but dangerous for shared entities (Employee shared across Departments).
Java
// ── Owned children — CascadeType.REMOVE is correct: ─────────────────
@Entity
public class Order {

    @OneToMany(mappedBy = "order",
               cascade = CascadeType.ALL,   // includes REMOVE
               fetch = FetchType.LAZY)
    private List<OrderItem> items = new ArrayList<>();
}

orderRepository.deleteById(1L);
// DELETE FROM order_items WHERE order_id = 1   (cascaded)
// DELETE FROM orders WHERE id = 1

// ── Hibernate loads children before deleting them: ────────────────────
// CascadeType.REMOVE triggers em.remove() on each child entity.
// Hibernate must load the collection to know what to delete.
// For large collections this is inefficient — prefer @Query bulk delete:
@Modifying
@Query("DELETE FROM OrderItem i WHERE i.order.id = :orderId")
void deleteByOrderId(@Param("orderId") Long orderId);

// ── DANGEROUS: CascadeType.REMOVE on shared entities ──────────────────
// WRONG — deleting a Department would delete all its Employees:
@Entity
public class Department {
    @OneToMany(mappedBy = "department",
               cascade = CascadeType.ALL)    // WRONG — employees are independent
    private List<Employee> employees;
}
departmentRepository.delete(department);
// → DELETE FROM employees WHERE department_id = ? (accidental mass delete!)

// ── CORRECT — no cascade on independent entities: ─────────────────────
@OneToMany(mappedBy = "department", fetch = FetchType.LAZY)
// No cascade — employees are independent; reassign them before deletion.
private List<Employee> employees;

// ── CascadeType.REMOVE vs orphanRemoval: ─────────────────────────────
// CascadeType.REMOVE — deletes children when the PARENT is deleted.
// orphanRemoval      — deletes children when they are REMOVED FROM THE COLLECTION.
// Both together (CascadeType.ALL + orphanRemoval = true) covers all cases:
//   parent deleted → children deleted (REMOVE cascade)
//   child unlinked → child deleted (orphanRemoval)

CascadeType.REFRESH and CascadeType.DETACH

CascadeType.REFRESH and CascadeType.DETACH are less commonly used. REFRESH propagates em.refresh() — reloading the entity's state from the database, discarding in-memory changes. DETACH propagates em.detach() — removing the entity from the persistence context so it is no longer tracked.
Java
// ── CascadeType.REFRESH ──────────────────────────────────────────────
// Propagates em.refresh() — reloads state from DB, discards dirty changes.
// Use when an entity's state may have been modified by an external process
// and you want to discard in-memory changes and reload from the database.

@OneToMany(mappedBy = "report",
           cascade = {CascadeType.PERSIST, CascadeType.MERGE,
                      CascadeType.REFRESH},
           fetch = FetchType.LAZY)
private List<ReportSection> sections;

// Usage:
Report report = reportRepository.findById(1L).orElseThrow();
report.getSections().get(0).setTitle("Modified title");  // not yet flushed

em.refresh(report);   // WITH CascadeType.REFRESH:
// → SELECT * FROM reports WHERE id = 1
// → SELECT * FROM report_sections WHERE report_id = 1
// Both report and sections reloaded — in-memory changes discarded.

// WITHOUT CascadeType.REFRESH: only the report is refreshed,
// sections retain their dirty in-memory state.

// ── CascadeType.DETACH ────────────────────────────────────────────────
// Propagates em.detach() — removes entity from the persistence context.
// After detach, changes to the entity are NOT tracked.
// Use when you want to pass an entity outside the transaction
// without it being a managed proxy.

@OneToMany(mappedBy = "document",
           cascade = {CascadeType.PERSIST, CascadeType.MERGE,
                      CascadeType.DETACH},
           fetch = FetchType.LAZY)
private List<DocumentPage> pages;

// Usage:
Document doc = documentRepository.findByIdWithPages(1L).orElseThrow();
em.detach(doc);   // WITH CascadeType.DETACH:
// → doc is detached
// → all pages are also detached
// Changes to doc.pages or any page are now untracked — no UPDATE generated.

// ── CascadeType.ALL = PERSIST + MERGE + REMOVE + REFRESH + DETACH ─────
// Convenient but includes REMOVE — make sure that is what you want.
// For most parent-child relationships: CascadeType.ALL is correct.
// For relationships with shared/independent entities: omit REMOVE.

orphanRemoval vs CascadeType.REMOVE

These two settings are often confused. They trigger in different scenarios and serve different purposes. Most owned parent-child relationships need both.
Java
// ── The key difference: ──────────────────────────────────────────────
//
// CascadeType.REMOVE fires when: em.remove(parent) is called
//   → parent is deleted → children are deleted
//
// orphanRemoval fires when: a child is removed from the parent's collection
//   → child is unlinked → child is deleted
//   → does NOT require the parent to be deleted

// ── Demonstration: ────────────────────────────────────────────────────
@OneToMany(mappedBy = "order",
           cascade = CascadeType.ALL,   // includes REMOVE
           orphanRemoval = true,
           fetch = FetchType.LAZY)
private List<OrderItem> items;

// Scenario 1: parent deleted (CascadeType.REMOVE fires)
orderRepository.deleteById(1L);
// → DELETE FROM order_items WHERE order_id = 1
// → DELETE FROM orders WHERE id = 1

// Scenario 2: child unlinked (orphanRemoval fires, parent NOT deleted)
@Transactional
public void removeFirstItem(Long orderId) {
    Order order = orderRepository.findByIdWithItems(orderId).orElseThrow();
    OrderItem first = order.getItems().get(0);
    order.removeItem(first);           // removes from collection
    orderRepository.save(order);
    // → DELETE FROM order_items WHERE id = ? (orphan is deleted)
    // → order itself is NOT deleted
}

// ── CascadeType.ALL without orphanRemoval: ────────────────────────────
@OneToMany(mappedBy = "order",
           cascade = CascadeType.ALL,
           orphanRemoval = false)        // default
private List<OrderItem> items;

order.removeItem(first);
orderRepository.save(order);
// → UPDATE order_items SET order_id = NULL WHERE id = ?
// (orphan survives — just loses its parent reference)

// ── orphanRemoval without CascadeType.REMOVE: ─────────────────────────
@OneToMany(mappedBy = "order",
           cascade = {CascadeType.PERSIST, CascadeType.MERGE},
           orphanRemoval = true)
private List<OrderItem> items;

order.removeItem(first);    // → DELETE orphan ✓
orderRepository.deleteById(orderId);
// Hibernate attempts to delete the order but the items still reference it
// → FK constraint violation! (items.order_id is NOT NULL)
// Must manually clear the collection first, or add CascadeType.REMOVE.

// ── Best practice for true parent-child (Order/OrderItem): ────────────
@OneToMany(mappedBy = "order",
           cascade = CascadeType.ALL,   // covers delete cascading
           orphanRemoval = true,        // covers unlinking
           fetch = FetchType.LAZY)
private List<OrderItem> items;

Cascade Type Decision Guide

Choosing the correct cascade type depends on the relationship between the entities — whether the child is owned by the parent, independent, or shared. This guide covers the most common relationship patterns.
Java
// ── PATTERN 1: Parent owns child (child has no identity outside parent)
// Example: Order → OrderItem, Post → Comment, Invoice → LineItem
// Child cannot exist without the parent.
// Parent controls the child's entire lifecycle.

@OneToMany(mappedBy = "order",
           cascade = CascadeType.ALL,
           orphanRemoval = true,
           fetch = FetchType.LAZY)
private List<OrderItem> items;
// PERSIST: new items saved with order
// MERGE:   detached items updated with order
// REMOVE:  items deleted when order deleted
// orphanRemoval: items deleted when unlinked

// ── PATTERN 2: Parent references independent entity (shared)
// Example: Order → Customer, Product → Category, Employee → Department
// The referenced entity exists independently and is not owned.
// Deleting the parent must NOT delete the referenced entity.

@ManyToOne(fetch = FetchType.LAZY)   // NO cascade
@JoinColumn(name = "customer_id")
private Customer customer;

@ManyToMany(fetch = FetchType.LAZY)  // NO cascade
@JoinTable(...)
private Set<Tag> tags;

// ── PATTERN 3: Parent creates child but does not own removal
// Example: User → Address (billing), where addresses may be reused
// Persist and merge cascade make sense; remove does not.

@OneToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE},
          fetch = FetchType.LAZY,
          orphanRemoval = false)
@JoinColumn(name = "billing_address_id")
private Address billingAddress;

// ── PATTERN 4: Audit / log relationship
// Example: Order → AuditLog
// Logs are appended (PERSIST), never modified (no MERGE), never deleted.

@OneToMany(mappedBy = "order",
           cascade = CascadeType.PERSIST,   // only persist
           fetch = FetchType.LAZY)
private List<AuditLog> auditLogs;

// ── Quick reference: ──────────────────────────────────────────────────
// Relationship type                Recommended cascade + orphanRemoval
// ─────────────────────────────────────────────────────────────────────
// Owned child (composition)      → CascadeType.ALL + orphanRemoval=true
// Shared/independent entity      → no cascade
// Create-together, delete-separate → CascadeType.PERSIST + MERGE
// Append-only log/audit           → CascadeType.PERSIST only
// @ManyToMany                     → no cascade (usually)

Common Cascade Mistakes

Incorrect cascade configuration is a leading cause of data loss, unexpected SELECTs, and constraint violations. These are the most frequent mistakes and how to avoid them.
Java
// ── MISTAKE 1: CascadeType.ALL on @ManyToOne to a shared entity ─────
// WRONG — deleting an Order would delete the Customer:
@ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;

// CORRECT — no cascade:
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customer_id")
private Customer customer;

// ── MISTAKE 2: CascadeType.ALL on @ManyToMany ─────────────────────────
// WRONG — deleting a Student would delete all their Tags,
// even though Tags may be shared with other Students:
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable(...)
private Set<Tag> tags;

// CORRECT — no cascade (join table entries are managed separately):
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(...)
private Set<Tag> tags;

// ── MISTAKE 3: Missing orphanRemoval with CascadeType.REMOVE ──────────
// WRONG — unlinking a child leaves an orphan row in the DB:
@OneToMany(mappedBy = "order",
           cascade = CascadeType.ALL,
           orphanRemoval = false)   // default — orphans survive
private List<OrderItem> items;

order.removeItem(item);   // FK set to NULL — orphan row remains
// CORRECT: add orphanRemoval = true

// ── MISTAKE 4: Cascade on the wrong (inverse) side ────────────────────
// WRONG — cascade on the mappedBy (inverse) side has no effect for REMOVE:
@Entity
public class OrderItem {
    @ManyToOne(cascade = CascadeType.REMOVE,   // won't cascade delete to Order
               fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;
}
// Cascade must be declared on the side that initiates the operation.
// Delete an Order → cascade to items (declare cascade on Order's @OneToMany).
// Delete an OrderItem → no cascade to Order (correct behaviour).

// ── MISTAKE 5: CascadeType.REMOVE causing FK violations ──────────────
// If children reference other entities with NOT NULL FKs,
// delete order may fail if items are not deleted before those references.
// Hibernate deletes in the order: parent first, then children — reversed.
// Solution: ensure Hibernate deletes children before the parent (it usually does),
// or use @OrderBy / manual deletion to control order.