Font Awesome Free 5.13.0 by @fontawesome - License - (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License)

Circular dependencies

Different kinds of circular dependencies, how to detect them and how to get rid of them if needed

Forms of circular dependencies

Between classes or files

  • Can lead to unexpected behavior, depending on the language
    • In Node.js, circular dependencies between files mean that one of the files will get an incomplete copy of the other on initialization (see CommonJS modules - Cycles )
  • Can lead to issues with dependency injection
    • If A's constructor needs an instance of B and B's constructor needs an instance of A, how will they ever both get constructed?
    • Some dependency injection frameworks cleverly work around this by passing proxy objects to the constructor and only instantiating the real objects when methods are called on those proxy objects (works fine as long as two objects don't call methods on each other in their constructors)
  • Sometimes unavoidable (for example, defining ORM classes for tables that hold a reference to each other)

Between folders, Java packages, ...

  • These are things that group files for organizational purposes but have no effect on deployment etc.
  • Circular dependencies between them could be a code smell but are often not an issue
  • Plenty of well-known open source libraries have circular dependencies at this level
  • Might be a cause for concern if you later want to turn these into separate developable and deployable components

Between deployable components

  • Independently developable, independently deployable
  • Circular dependencies at this level are bad, since they prevent the components from really being independently developable and deployable (and testable)
  • Might even lead to problems in building the components, depending on the language
  • See Deployable components - Acyclic Dependencies Principle

Between microservices

  • Somewhat similar to deployable components
  • No compile time dependencies, so likely no issues in building the services
  • Breaking changes require careful coordination, but this is always the case with microservices (since depending services have no control over when they start using the new version of the service)
  • If a service fails because of a failure in an upstream dependency, it's hard to find the root cause if the service that failed is part of a cycle (the bigger the cycle, the harder this can get)

Detecting them

See Static analysis - Internal dependencies

Getting rid of them

Potential approaches:

  • Reverse the direction of a dependency using the Dependency Inversion Principle (DIP) (see Dependency inversion principle (DIP))
  • Reverse the direction of a dependency using events
    • Before: A depends on B and sends commands to it
    • After: B depends on A and listens to events from it
  • Extract something new that involved parts can depend on instead of depending on each other
    • It's possible that some of the involved parts have more than one responsibility and that extracting that responsibility automatically solves the circular dependency
    • Special case: splitting CRUD functionality into separate read functionality and create/update/delete functionality
      • This way, create/update/delete functionality for both A and B can depend on read functionality for both A and B without creating circular dependencies
  • Merge the involved parts together
    • Example: a circular dependency between microservices might be a sign that the splitting has been taken too far and that the involved microservices should actually become one service