hibernate/hibernate-orm · error · IllegalStateException

Unbreakable cycle detected for SCC: %s

Error message

Unbreakable cycle detected for SCC: %s

What it means

While making the flush dependency graph schedulable, CycleBreaker found a cycle with no edge it is allowed to break: no preferred-order edge, no update edge, and hasAnyBreakableEdge() reports nothing breakable at all. Every dependency in the cycle is a required (non-nullable, non-deferrable) FK/unique constraint, so any execution order would violate at least one constraint at flush time. Hibernate refuses to schedule doomed SQL and throws this IllegalStateException instead.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/action/queue/internal/plan/CycleBreaker.java:132

				}
				continue;
			}

			final GraphEdge chosen = chooseEdgeToBreak(cycle);
			if (chosen == null) {
				if (isDeleteOnlyCycle(cycle)) {
					breakArbitraryDeleteEdge(cycle, deferrableConstraintMode);
					continue;
				}

				final GraphEdge updateEdge = findUpdateEdge(cycle);
				if (updateEdge != null) {
					updateEdge.setBroken(true);
					continue;
				}

				if (!hasAnyBreakableEdge(cycle)) {
					throw new IllegalStateException("Unbreakable cycle detected for SCC: " + describeScc(scc));
				}

				final GraphEdge deferrableEdge = findEffectivelyDeferredEdge(cycle, deferrableConstraintMode);
				if (deferrableEdge != null) {
					deferrableEdge.setBroken(true);
					continue;
				}

				// Break a non-DELETE edge as last resort.
				final GraphEdge nonDeleteEdge = findNonDeleteEdge(cycle);
				if (nonDeleteEdge != null) {
					nonDeleteEdge.setBroken(true);
					// Only explicit patchable edges actually install a patch; required
					// edges return null from GraphEdge#getPatchCycleType().
					if (nonDeleteEdge.getPatchNode() != null &&
						nonDeleteEdge.getPatchNode().group().kind() == MutationKind.INSERT) {
						installPatchForEdge(nonDeleteEdge);
					}

View on GitHub (pinned to fad1729dce)

Solutions

  1. Make at least one association in the cycle nullable (remove optional=false / nullable=false) so the planner can apply NULL-then-patch.
  2. Restructure the model to break the cycle: make one direction unidirectional or move the FK to a nullable join table.
  3. Assign in phases: persist A with the reference null, flush, then set B.a / A.b and flush again.
  4. On PostgreSQL/Oracle, declare the FKs DEFERRABLE INITIALLY DEFERRED and enable Hibernate's deferrable constraint mode so edges in the cycle can be broken.

Example fix

// before - mutual NOT NULL FKs, unbreakable cycle
@Entity class A { @ManyToOne(optional = false) B b; }
@Entity class B { @ManyToOne(optional = false) A a; }
// after - one side may be temporarily null
@Entity class A { @ManyToOne(optional = false) B b; }
@Entity class B { @ManyToOne A a; }
Defensive patterns

Strategy: validation

Validate before calling

// phase inserts so the NOT NULL cycle never exists within one flush
A a = new A();
B b = new B();
a.setB(b);          // B.a stays null for now
session.persist(a);
session.persist(b);
session.flush();
b.setA(a);          // close the cycle after both rows exist

Try / catch

try {
    session.flush();
}
catch (HibernateException e) {
    if (e.getMessage() != null && e.getMessage().contains("Unbreakable cycle detected for SCC")) {
        tx.rollback(); // reapply changes in phases with intermediate flushes
    }
    else throw e;
}

Prevention

When it happens

Trigger: Inserting or updating entities that reference each other through NOT NULL foreign keys in one transaction, e.g. @ManyToOne(optional=false) on both directions of A<->B, or a self-referential entity with a mandatory parent; schemas where FKs previously were deferrable but no longer are.

Common situations: Bidirectional @ManyToOne/@OneToOne marked optional=false on both sides; schema export produced NOT NULL FK columns on both tables; migrating from the legacy action queue whose ordering worked by accident; MySQL/MariaDB which lack deferred FK checks.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/b647a8ecc3adf1be. Report an issue: GitHub.