Circular dependency
A circular dependency in software engineering refers to a relation between two or more modules, classes, or program units that depend on each other either directly or indirectly, forming a cycle in the dependency graph.[1] This cycle arises when, for instance, module A requires definitions or functionality from module B, while module B simultaneously requires those from module A, preventing independent compilation or execution.[2] Such dependencies are common in object-oriented designs and aspect-oriented programming, where they manifest as bad smells that degrade modularity and reusability.[3] Circular dependencies pose significant challenges during software development and maintenance, as they can trigger errors in build tools likemake, complicate testing by requiring combined module evaluation, and increase coupling that hinders code reuse.[4][1] In languages such as C++, they often lead to compilation failures due to unresolved references, whereas Java may tolerate them at runtime.[4] Beyond core programming, circular dependencies appear in related areas like goal-oriented requirements modeling, where mutually dependent goals create cycles that undermine model quality and validation.[5]
To mitigate circular dependencies, developers employ strategies such as introducing interfaces or abstract classes to break cycles, refactoring operations to a single owning module, or restructuring components into uni-directional relationships.[4][2] Detection tools, including search-based algorithms like simulated annealing, help identify these cycles in large codebases or models, enabling proactive resolution.[5] In aspect-oriented contexts, treating cyclically dependent aspects as a unified group or transforming them into orthogonal units preserves functionality while reducing tight coupling.[3] Overall, avoiding or eliminating circular dependencies promotes cleaner, more maintainable architectures in complex software systems.
Fundamentals
Definition
A circular dependency, also referred to as a cyclic dependency, occurs when two or more entities—such as modules, components, or processes—in a software system depend on each other, either directly or indirectly, forming a cycle in the dependency graph that hinders independent resolution or initialization.[6] This cycle creates tight coupling between the entities, violating principles like the Acyclic Dependencies Principle, which advocates for dependency structures that allow for modular design and easier maintenance.[7] Key characteristics of circular dependencies include their manifestation as cycles in directed graphs: direct cycles involve mutual dependencies between two entities (e.g., entity A depends on B, and B on A), while indirect cycles arise through a chain of dependencies (e.g., A depends on B, B on C, and C on A).[6] In contrast, acyclic dependencies form a directed acyclic graph (DAG), where no such cycles exist, enabling straightforward ordering and processing of entities.[8] Understanding circular dependencies requires familiarity with prerequisite concepts in graph theory applied to computing. A dependency graph is a directed graph where nodes represent system entities and edges denote directional dependencies from one entity to another it relies upon.[8] Topological sorting is an algorithm that linearizes the nodes of a DAG such that for every directed edge from node U to V, U precedes V in the ordering, facilitating tasks like build order resolution or task scheduling; however, the presence of cycles renders topological sorting impossible, as no valid linear order exists.[9]Basic Examples
A classic everyday analogy for a circular dependency is the "chicken-or-egg" dilemma, where the chicken must exist to lay the egg, but the egg must exist to hatch the chicken, creating an interdependent loop with no clear starting point.[10] This illustrates how circular dependencies can stall progress by requiring mutual prerequisites that cannot be resolved sequentially. In a simple technical context, consider two software modules or packages where module A requires functionality from module B to operate, while module B similarly requires module A; for example, packagecom.example.a imports com.example.b, and com.example.b imports com.example.a. This mutual reliance forms a cycle during compilation or loading, preventing the system from initializing either module first, as each awaits the other's completion.
Such relationships are often represented in a dependency graph, where nodes denote components (e.g., modules A and B) and directed edges show dependencies (e.g., A → B means A depends on B). A non-circular, linear dependency might appear as:
This allows topological sorting for resolution, starting with B then A. In contrast, a circular dependency forms a loop:A → BA → B
This cycle, akin to the formal definition of a dependency relation containing a loop, defies linear processing and requires intervention to break.[11]A → B ↑ ↓ B ← AA → B ↑ ↓ B ← A
Applications in Computing
In Programming and Modules
In object-oriented programming, a circular dependency arises when two or more classes mutually depend on each other, such as class A requiring an instance of class B in its constructor and class B requiring an instance of class A, which can result in infinite recursion during object instantiation or undefined behavior at runtime.[12] Empirical studies of Java applications indicate that such cycles are prevalent, with 45% of sufficiently large programs containing cycles involving at least 100 classes, often signaling design issues rather than inherent necessities.[12] These cycles increase tight coupling, complicating maintenance and testing by propagating changes across dependent classes.[13] Specific behaviors vary by programming language. In Java, circular dependencies at the class level are tolerated during class loading due to lazy initialization, but package-level cycles—where packages mutually import classes from one another—are strongly discouraged, as they form non-directed acyclic graphs, leading to fragile builds, reduced reusability, and potential runtime errors likeClassNotFoundError.[13] For instance, if package account imports from package user and vice versa, refactoring one package may require simultaneous changes to the other, hindering independent development.[13] In Python, mutual import statements between modules can trigger an ImportError if one module attempts to access attributes from the other before the importing module is fully executed, as the import system adds partially initialized modules to sys.modules to avoid unbounded recursion but raises exceptions for undefined attributes.[14] An example is two modules foo.py and bar.py where foo imports bar and immediately uses a bar attribute, while bar imports foo; this fails with ImportError: cannot import name 'attribute' from partially initialized module 'foo'.[14] In C++, circular inclusions between header files cause compilation failures due to infinite expansion loops, unless mitigated by forward declarations for pointers or references, which allow incomplete type usage without full definitions.[15] For example, if A.h includes B.h and B.h includes A.h, the compiler reports errors like "invalid use of incomplete type"; resolving this involves declaring class B; in A.h and using B* instead of full objects.[15]
Module systems in package managers also address circular dependencies through detection and resolution mechanisms. In Node.js with npm, the CommonJS module system permits cyclic dependencies by returning a partially constructed exports object during require() calls, preventing infinite loops while allowing modules to complete loading sequentially.[16] For instance, if module A requires B and B requires A mid-execution, B receives A's unfinished exports, but once B finishes, A continues, ensuring both are usable afterward—though this requires careful design to avoid accessing uninitialized properties.[16] Maven, for Java projects, detects cyclic dependencies during transitive resolution and reports them as build errors, enforcing acyclic graphs to maintain reproducible builds; the Maven Enforcer Plugin's banCircularDependencies rule explicitly scans direct and transitive dependencies across specified scopes, failing the build if cycles are found.[17][18] This configuration might specify scopes like compile and test to ignore optional dependencies while flagging cycles in core artifacts.[18]
In System Design and Architecture
In microservices architectures, circular dependencies arise when one service relies on another for functionality, and the second service reciprocally depends on the first, often manifesting as synchronous API calls that form a cycle, such as Service A invoking Service B, which then calls back to Service A.[19] This can lead to deployment or scaling deadlocks, where independent scaling of services becomes impossible due to the intertwined nature of their operations, complicating fault isolation and overall system resilience.[19] To mitigate such issues, architects often refactor by introducing asynchronous messaging or shared data layers to break the cycle without merging services.[20] Circular dependencies also violate core principles in established design patterns, particularly in layered architectures where the dependency rule mandates that outer layers (e.g., presentation) depend only on inner layers (e.g., business logic and data access), preventing loops such as the data layer indirectly depending back on the presentation layer through shared interfaces.[21] In dependency injection frameworks like Spring, these cycles occur when beans form mutual references—Bean A injected into Bean B, and vice versa—potentially halting application startup if resolved via constructor injection; Spring addresses this by favoring setter injection or lazy initialization to allow partial bean creation before full wiring.[22] Such violations undermine modularity, as they blur boundaries intended by patterns like MVC or Clean Architecture.[23] In enterprise environments, circular references frequently appear in database schemas through foreign keys that create cycles, for instance, Table A referencing Table B via a foreign key while Table B references Table A, which enforces referential integrity but complicates data insertion and deletion by requiring deferred constraint checks until transaction commit.[24] Similarly, in cloud architectures, API dependencies can form cycles when services in platforms like AWS expose endpoints that mutually invoke each other, or when infrastructure-as-code tools like CloudFormation define resources with reciprocal references (e.g., an EC2 instance depending on a security group that depends back on the instance), stalling stack deployments until the cycle is broken via explicit ordering or resource refactoring.[25] These patterns extend module-level cycles observed in programming but amplify risks at scale due to distributed latency and failure propagation.[19]Consequences
Technical Challenges
Circular dependencies often manifest as build-time issues, where the compilation or linking process fails due to unresolved references among interdependent modules. In C++, for instance, circular dependencies between static libraries can lead to linker errors, as the linker requires a specific order to resolve symbols but cannot determine it when libraries depend on each other mutually, resulting in undefined reference complaints.[26] Similarly, in JavaScript projects using bundlers like Webpack, circular imports can trigger errors or warnings during the bundling phase, where modules are partially resolved, leading to failures in generating complete bundles or runtime-undefined variables if not detected.[27] At runtime, circular dependencies can precipitate severe problems such as infinite loops during object initialization or stack overflows from recursive invocations. In dependency injection frameworks like Spring, constructor-based circular dependencies cause the container to enter an endless creation cycle—attempting to instantiate Bean A requires Bean B, which in turn requires Bean A—resulting in aBeanCurrentlyInCreationException and preventing application startup.[22][23] This recursion can escalate to stack overflows in languages like .NET, where the service provider recursively resolves services in a loop during startup, exhausting the call stack without explicit cycle detection.[28]
Testing circular dependencies introduces significant complications, as isolating components for unit tests becomes infeasible without resolving the cycle, often forcing reliance on integration tests or brittle mocks that replicate the entire dependency graph. In Spring applications, for example, unit tests fail to load the application context due to the same BeanCurrentlyInCreationException encountered at runtime, requiring additional annotations like @Lazy or redesigns to enable test execution, which undermines test isolation and reliability.[23] This makes mocking individual dependencies challenging, as the cycle prevents independent instantiation, leading to fragile tests that mirror production behavior rather than verifying isolated logic.