CIRCULAR_DEPENDENCY: Apex class A depends on B, which depends on A
Two Apex classes refer to each other in a way that the platform can't compile in isolation. The deploy fails because there's no order in which both classes can compile cleanly. The fix is breaking the cycle with an interface, an abstract class, or a third class that holds the shared logic.
Also seen asCIRCULAR_DEPENDENCY·circular dependency apex·depends on which depends on·Apex class circular reference
Friday afternoon, the release team kicks off the production deploy. The package validation runs for fifteen minutes, then fails: CIRCULAR_DEPENDENCY: Apex class CaseTrigger depends on CaseService, which depends on CaseTrigger. Both classes are in the deploy. Both compile cleanly in the sandbox. The deploy rolls back and the team is back to staring at a dependency graph trying to figure out where the cycle is.
What the platform checks
When you deploy Apex, the metadata API validates that every class, trigger, page, and component can compile in isolation and in the order the deploy specifies. The validator builds a directed graph: nodes are Apex classes, edges go from each class to every other class it references. If the graph contains a cycle, the validator rejects the deploy with CIRCULAR_DEPENDENCY and names two of the classes involved.
The message names exactly two classes (the head and the tail of the back-edge the validator detected), but the cycle might be longer. The two names in the message tell you where to start tracing, not the full length of the cycle.
Circular dependencies are forbidden because Apex needs a stable load order. The compiler has to decide which class to validate first. If A references B and B references A, neither can be validated before the other. The platform refuses to ship code in this state, even if a particular runtime execution path would work fine, because the static dependency graph would be ambiguous.
The broken example
A common shape: a trigger handler calls into a service class that, somewhere deep in its code, calls back into a method on the trigger handler:
public class CaseTriggerHandler {
public static void afterInsert(List<Case> newCases) {
CaseService.notifyAccountOwner(newCases);
for (Case c : newCases) {
if (c.Priority == 'High') {
escalate(c);
}
}
}
public static void escalate(Case c) {
// Send escalation email, create task, etc.
CaseService.assignToEscalationQueue(c);
}
}
public class CaseService {
public static void notifyAccountOwner(List<Case> cases) {
// Lookup account owner, post Chatter, etc.
}
public static void assignToEscalationQueue(Case c) {
// Determine queue, then re-check the priority via the trigger helper
if (CaseTriggerHandler.isReallyHighPriority(c)) {
// assignment logic
}
}
}
CaseTriggerHandler.afterInsert calls CaseService.notifyAccountOwner and CaseService.assignToEscalationQueue. CaseService.assignToEscalationQueue calls back into CaseTriggerHandler.isReallyHighPriority. The graph has two nodes with an edge each direction. The deploy fails with CIRCULAR_DEPENDENCY.
A second shape that surprises developers: a class extends a class that, somewhere in the inheritance chain, references the subclass. Inheritance creates a hard reference at compile time, so even a static field of the parent that holds a list of subclass instances counts as a dependency.
A third shape: two test classes that share a @TestSetup utility, and the utility references both test classes' helpers. This one is especially nasty because tests don't run in production, but they're still validated at deploy time, so the cycle blocks the deploy.
Why the dependency graph cares about every reference
The Apex compiler tracks every static reference: type references in variable declarations, type references in method signatures, static method calls, type references in instanceof checks, references in Type.forName calls when the literal class name is in the source.
Dynamic references via reflection (Type.forName('CaseService') with a runtime-computed string) don't show up in the static graph, so they don't create dependencies. That's both a feature (you can break a cycle by introducing dynamic dispatch) and a footgun (the dependency exists at runtime but the compiler can't see it, so it can't validate either).
Inner classes count too. A reference to CaseService.QueueRouter from CaseTriggerHandler is a reference to CaseService itself. Refactoring an inner class into a top-level class doesn't change anything; the outer class still owns the inner one.
The fix: break the cycle, three paths
There are three common ways to break the cycle, in order of likelihood that they'll fit your situation.
Extract a third class that both depend on. The most common cure for a two-node cycle is to introduce a third class that holds the shared logic. Both original classes depend on the new one, and the new one depends on neither.
In the example above, CaseTriggerHandler.isReallyHighPriority is the back-edge. Move that method into a new class CasePriorityRules:
public class CasePriorityRules {
public static Boolean isReallyHighPriority(Case c) {
return c.Priority == 'High' && c.IsClosed == false;
}
}
CaseTriggerHandler and CaseService both depend on CasePriorityRules. CasePriorityRules depends on neither. The cycle is gone.
Inline the small caller. If the back-edge calls a single short method, inline that method into the calling class. Duplication is sometimes the right answer when the alternative is a structural defect.
Invert the dependency. Sometimes the back-edge represents the wrong direction. If CaseService only calls back into CaseTriggerHandler to get configuration, the configuration should live in a constants class or a custom metadata type that CaseService reads directly. Inverting the dependency removes the cycle and clarifies the design.
The fixed example
After the extract-shared-class refactor:
public class CasePriorityRules {
public static Boolean isReallyHighPriority(Case c) {
return c.Priority == 'High' && c.IsClosed == false;
}
}
public class CaseTriggerHandler {
public static void afterInsert(List<Case> newCases) {
CaseService.notifyAccountOwner(newCases);
for (Case c : newCases) {
if (CasePriorityRules.isReallyHighPriority(c)) {
escalate(c);
}
}
}
public static void escalate(Case c) {
CaseService.assignToEscalationQueue(c);
}
}
public class CaseService {
public static void notifyAccountOwner(List<Case> cases) {
// posts Chatter
}
public static void assignToEscalationQueue(Case c) {
if (CasePriorityRules.isReallyHighPriority(c)) {
// assignment logic
}
}
}
The dependency graph now looks like: CaseTriggerHandler and CaseService both depend on CasePriorityRules. No cycle. The deploy succeeds.
Tracing a long cycle
The platform's error message names two classes. If you can't see how those two depend on each other, the cycle is longer than two nodes. To trace it, build a dependency map from your source:
sf project deploy validate --source-dir force-app --dry-run
or run a static analysis tool that visualizes Apex dependencies. The Salesforce Code Analyzer's PMD rules include cycle detection. Look for the smallest cycle that contains both of the named classes; that's your target.
For manual tracing in the Developer Console, use the "Class Dependency" view (right-click the class, Open Type Hierarchy). The view shows incoming and outgoing references. Walk the graph from one of the named classes, following outgoing references, until you find a path back to the second named class.
Why the cycle shows up at deploy time, not at compile time in the IDE
Local Apex tools (VS Code with the Salesforce CLI extensions, the Developer Console) do incremental compilation that doesn't always rebuild the full dependency graph. A reference added inside an existing class might not retrigger the cycle check until you deploy the full set of changes together.
The metadata API's deploy validator always builds the full graph. That's why the cycle surfaces at deploy time even though the individual files compile in the editor.
A common workflow that hits this: developer A edits CaseTriggerHandler and adds a reference to CaseService.assignToEscalationQueue. Developer B edits CaseService and adds a reference to CaseTriggerHandler.isReallyHighPriority. Neither edit, in isolation, creates a cycle. Both edits, deployed together, do. The deploy is the first time the combination is tested.
The mitigation: run sf project deploy validate --check-only in CI on every pull request. The validator runs the full graph check without committing the deploy, so cycles surface at PR time.
When the cycle involves managed package classes
If one of the classes is in an installed managed package, you can't refactor it. The fix has to live entirely on your side.
The usual cure is to break the dependency from your code to the managed class by introducing a dynamic dispatch layer. Wrap the managed class's call in a thin local class:
public class ManagedPackageAdapter {
public static void doThing(Id recordId) {
managed_namespace.ManagedClass.publicMethod(recordId);
}
}
Your other classes call ManagedPackageAdapter instead of the managed class directly. If the cycle involves a back-edge from the managed class into yours, that back-edge has to be removed; managed packages cannot have circular dependencies with your code, but unmanaged code can sometimes pull a managed class's dependency onto itself transitively.
Trigger framework patterns and cycle prevention
A well-designed trigger framework prevents most circular-dependency issues by structure. The pattern: each trigger has a single Handler class that orchestrates business logic, and the business logic lives in Service classes that don't know about triggers.
Trigger (one per object)
→ Handler (one per trigger, orchestrates)
→ Service (business logic, no trigger references)
→ Domain helpers (queries, validations)
References go strictly downward. Handlers reference Services. Services reference Domain helpers. Nothing references back up. Cycles become structurally impossible.
Most legacy orgs accumulate cycles when business logic creeps from Services back up into Handlers ("just call the trigger helper for this one case"). Discipline at the architectural layer prevents the cycle from forming.
Closely related deploy failures
| Error | Cause |
|---|---|
CIRCULAR_DEPENDENCY: A depends on B, which depends on A | Static reference graph has a cycle |
INVALID_TYPE: type does not exist | Reference to a class not in the deploy and not already in the org |
Dependent class is invalid and needs recompilation | A class the deploy depends on failed to compile; fix the underlying class first |
Method does not exist or incorrect signature | Calling a method that doesn't exist in the version of the dependency in the deploy |
The first three are graph-related, the fourth is signature-related. All four block deploys and require fixing before code can ship.
Testing for cycles in CI
A simple cycle check in CI:
sf project deploy validate --source-dir force-app --test-level NoTestRun --check-only
This validates the metadata without running tests. If the deploy graph has a cycle, it fails fast. Run it on every pull request that touches Apex.
For more thorough testing, run it with --test-level RunLocalTests to also catch test-class dependency cycles, which are easy to introduce when test utilities reference helper classes that reference the tests.
Defensive habits
Keep the dependency graph shallow. Services don't reference Handlers. Domain helpers don't reference Services. Static analysis catches the rest.
Avoid two-way references between any pair of classes. If A and B both need to know about each other, one of them probably owns logic that should live in a third class.
Prefer dependency injection or interface-based dispatch where two classes genuinely need to talk to each other but the dependency graph can't accommodate the direct reference.
Run sf project deploy validate on every PR. Cycles introduced by combined edits surface at PR time, not Friday afternoon.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Deployment errors
ALREADY_IN_PROCESS: another deployment is in progress
DeploymentAn org can run only one deployment at a time. Your deploy is queued behind a deployment someone (or some automation) already started. Wait f…
ApexUnitTestClassShouldHaveAsserts / ApexBadCrypto / Code Analyzer violations blocking deploy
DeploymentSalesforce's open-source code scanners (PMD-Apex, Salesforce Code Analyzer) found rule violations in your codebase. They don't block deploys…
Average test coverage across all Apex Classes and Triggers is XX%, at least 75% required
DeploymentA production deploy of Apex requires at least 75% line coverage *org-wide*, and 100% on triggers (including triggers that aren't part of you…
BadElement / Element type required / package.xml is malformed
DeploymentYour `package.xml` (or `destructiveChanges.xml`) is invalid XML or references a metadata type the platform doesn't recognise. The error name…
Cannot change field type from <type1> to <type2> via the API
DeploymentSalesforce restricts which field type changes are allowed in a deploy. Many type transitions (Number to Text, Picklist to Lookup, Text to Lo…