Java  

How to Detect Circular Dependency in Java Modules

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:

  1. Tight Coupling

    • Modules become interdependent, making changes difficult.

  2. Build or Compile Failures

    • The Java compiler and modular system may refuse to compile modules with cyclic requires statements.

  3. Complex Testing

    • Unit testing becomes difficult because one module can’t be tested independently.

  4. Runtime Errors

    • Cycles can cause unexpected initialization order or null references during module loading.

  5. Reduced Reusability

    • Modules tied together by circular dependencies can’t be reused easily in other projects.

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

  • Use the Dependency Analysis View under the Plug-in Development Environment (PDE).

  • You can visualize inter-module dependencies and spot cycles easily.

Maven or Gradle

If you’re using a multi-module Maven or Gradle project:

  • Check for recursive dependencies in the pom.xml or build.gradle files.

  • Maven will warn you if two modules depend on each other directly.

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:

  1. Represent all modules and their dependencies in a map (Adjacency List).

  2. Use two sets:

    • visited — marks nodes already visited.

    • recStack — marks nodes currently in the recursion stack.

  3. For each unvisited node, perform DFS:

    • Mark it visited and add to recursion stack.

    • For each dependency:

      • If not visited, DFS on it.

      • If already in recursion stack → cycle found.

    • Remove node from recursion stack when finished.

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

  • Observer Pattern or Event-driven architecture can reduce direct dependencies.

  • Factory Pattern helps avoid circular constructor dependencies.

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:

  • CustomerModule handles customer profiles.

  • AccountModule manages bank accounts.

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.