hibernate/hibernate-orm · error · IllegalStateException

Unbreakable cycle detected for SCC

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.

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.

Appendix: 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)