Introduction
In Java development, especially in modular or multi-module projects, dependencies define how modules or components rely on each other. A circular dependency happens when two or more modules depend on each other either directly or indirectly, forming a loop.
For example, if Module A depends on Module B, and Module B depends back on Module A, a circular relationship is created.
Circular dependencies can make your project harder to maintain, increase build times, and even cause runtime or compilation errors.
In this article, we’ll understand what circular dependencies are, why they are problematic, and how to detect and fix them using simple logic and Java code examples.
What is a Circular Dependency?
A circular dependency (also known as a cyclic dependency) occurs when a group of modules depend on each other in such a way that they form a closed loop.
Let’s take an example:
Module A → depends on Module B
Module B → depends on Module C
Module C → depends on Module A
This creates a cycle: A → B → C → A
Such dependencies cause several problems, especially in modular systems introduced from Java 9 and above, where module-info.java
defines inter-module relationships.
Why Circular Dependencies Are a Problem
Circular dependencies create many issues in software projects:
Tight Coupling
Build or Compile Failures
Complex Testing
Runtime Errors
Reduced Reusability
Types of Circular Dependencies
There are two major levels where circular dependencies appear:
1. Class-Level Circular Dependency
Occurs when two or more classes directly or indirectly depend on each other.
For example
// File: A.java
public class A {
private B b = new B();
}
// File: B.java
public class B {
private A a = new A();
}
This leads to a compile-time or runtime issue depending on object initialization.
2. Module-Level Circular Dependency
Occurs when two Java modules depend on each other in their module-info.java
files.
Example
// In module A
module com.example.A {
requires com.example.B;
}
// In module B
module com.example.B {
requires com.example.A;
}
This direct cycle will result in a compiler error:
“Cycles in module dependencies are not allowed.”
How to Detect Circular Dependencies
There are two main ways to detect circular dependencies: manually using tools or programmatically using algorithms.
1. Detecting Using IDE or Build Tools
Most popular IDEs and build tools can automatically detect dependency cycles.
IntelliJ IDEA
Go to Analyze → Analyze Dependencies.
Select your modules or packages.
The tool shows a graph; if you see loops between modules, those represent circular dependencies.
Eclipse
Maven or Gradle
If you’re using a multi-module Maven or Gradle project:
These manual methods are effective for small to medium projects.
2. Detecting Using a Java Program (Graph Algorithm)
For larger or dynamic systems, you can detect circular dependencies programmatically by modeling modules as nodes in a graph.
Each module is a node, and each dependency is a directed edge (A → B
).
If there’s a cycle in the graph, it means there’s a circular dependency.
This is a classic cycle detection problem in a directed graph, solved using Depth-First Search (DFS) with a recursion stack.
Step-by-step logic:
Represent all modules and their dependencies in a map (Adjacency List).
Use two sets:
For each unvisited node, perform DFS:
Java Implementation Example
import java.util.*;
public class CircularDependencyDetector {
private final Map<String, List<String>> graph = new HashMap<>();
private final Set<String> visited = new HashSet<>();
private final Set<String> recStack = new HashSet<>();
public void addDependency(String module, String dependency) {
graph.computeIfAbsent(module, k -> new ArrayList<>()).add(dependency);
}
public boolean hasCircularDependency() {
for (String module : graph.keySet()) {
if (detectCycle(module)) {
return true;
}
}
return false;
}
private boolean detectCycle(String module) {
if (recStack.contains(module)) return true;
if (visited.contains(module)) return false;
visited.add(module);
recStack.add(module);
for (String dep : graph.getOrDefault(module, new ArrayList<>())) {
if (detectCycle(dep)) return true;
}
recStack.remove(module);
return false;
}
public static void main(String[] args) {
CircularDependencyDetector detector = new CircularDependencyDetector();
detector.addDependency("A", "B");
detector.addDependency("B", "C");
detector.addDependency("C", "A"); // Creates a cycle
if (detector.hasCircularDependency()) {
System.out.println("Circular dependency detected!");
} else {
System.out.println("No circular dependencies found.");
}
}
}
Output
Circular dependency detected!
This algorithm can detect cycles in any directed dependency graph, whether between Java classes, packages, or modules.
How to Fix Circular Dependencies
Once detected, the next step is to remove or refactor them.
1. Use Interfaces or Abstractions
Move shared logic into an interface or a separate module that both modules depend on, instead of depending on each other.
2. Create a Common or Utility Module
If both modules share common functionality, extract it into a third module that both can depend on.
3. Apply the Dependency Inversion Principle
Let higher-level modules depend on abstractions rather than concrete implementations.
4. Use Design Patterns
5. Refactor or Merge Modules
If two modules are too tightly connected, consider merging them into one logical unit to eliminate the cycle.
Example of Breaking a Cycle
Before
// Module A
module com.app.A {
requires com.app.B;
}
// Module B
module com.app.B {
requires com.app.A;
}
After fixing
// Common module
module com.app.common { }
// Module A
module com.app.A {
requires com.app.common;
}
// Module B
module com.app.B {
requires com.app.common;
}
By introducing a common module, both A and B now depend only on shared code instead of each other — breaking the cycle.
Real-life Example
Imagine you have two modules in a banking application:
If CustomerModule
calls AccountService
, and AccountModule
also calls CustomerService
, you have a circular dependency.
Fix: Create a BankCommon
module with shared interfaces like ICustomer
and IAccount
, and let both modules depend on it instead of directly on each other.
Summary
A circular dependency occurs when two or more modules depend on each other directly or indirectly.
It causes build errors, runtime issues, and maintenance challenges.
You can detect cycles manually using IDE tools or programmatically using a DFS-based graph algorithm.
Breaking cycles involves refactoring modules, introducing interfaces, or creating common shared modules.
Keeping your dependency graph acyclic ensures cleaner, more maintainable, and scalable Java architecture.