In the previous article, we summarized modularization as “grouping things that change together to establish boundaries.”
But the real problem starts after splitting modules. The pieces begin referencing one another.
A uses B, B uses C, and one day C uses A again. All the benefits of modularization disappear because the three are effectively one unit.
The short answer: module dependency design has two rules. Keep dependency flow one-way, and make frequently changing parts depend on stable ones.
This article covers how to choose dependency direction, typical paths to circular dependencies, and three ways to break them.
Which way should dependencies flow?
Dependencies have direction. If module A imports module B, A depends on B.
There is a classic answer to where the arrow should point.
The unstable should depend on the stable. Reverse that, and a minor change spreads through the entire system.
A stable module changes infrequently—domain models and shared protocols, for example.
An unstable module changes often: UI and requirement-driven event-handling logic, for example.
So a healthy dependency graph generally flows like this.
- Feature modules → domain modules → shared interface modules
- Arrows flow only downward, toward stability; they never point back up.
Robert C. Martin called this the Stable Dependencies Principle (SDP).
Why do circular dependencies arise?
Circular dependencies usually arise naturally, without malicious intent. A typical path looks like this.
The Order module references the User module because orders need purchaser information.
Later, a request arrives to show “recent orders” on the member screen. The moment User references Order, the cycle is complete.
Three things break when a cycle appears.
- Build-unit separation fails: the compiler cannot process them independently, so they effectively become one module (Swift blocks cross-module circular imports with a compile error).
- Independent testing becomes impossible: testing A requires B, while testing B requires A—a deadlock.
- Impact becomes unpredictable: fixing either side requires reviewing the other.
How do you break circular dependencies?
There are three main approaches used in practice.
First, move shared pieces downward.
If both sides really need “part of the other,” extract that part into a more stable lower-level module. For example, move only the types Order and User share into a domain model module.
Second, reverse the direction with interfaces (DIP).
Replace one dependency with a protocol. User defines only a protocol for “something that provides an order list,” while Order supplies the implementation.
// User Module: declare only the required capabilities as a protocol
public protocol OrderHistoryProviding {
func recentOrders(of userID: String) -> [OrderSummary]
}
// Order Module: User Adopt the module’s protocol and implement it
public struct OrderHistoryProvider: OrderHistoryProviding {
public func recentOrders(of userID: String) -> [OrderSummary] {
// Query the order repository
}
}
Now only one arrow remains: Order → User. Dependency inversion from the earlier DIP article is applied directly at the module level.
Third, move assembly upward.
Instead of connecting the modules directly, an upper module that knows both—the app target or assembly layer—connects them. This is especially useful when feature modules need to invoke one another for navigation.
| Situation | How to break it |
|---|---|
| Each side needs only part of the other’s types | Extract shared types into a lower-level module |
| One side calls the other’s functionality | Define a protocol, then invert the dependency |
| Navigation between feature modules | Connect them in an upper assembly layer |
When should you apply this, and when is it excessive?
Managing dependency direction also costs something: more protocols and assembly code.
- With 3–4 or more modules and separate teams: document direction rules and monitor cycles with tools.
- For a small project with two modules: just avoid cycles. Wrapping every reference in a protocol is excessive.
- For legacy code that already has cycles: do not break everything at once; clean up arrows starting with the most frequently changing module.
This is how interviewers ask it
Q. Why are circular dependencies between modules a problem, and how would you solve them?
A cycle makes two modules one unit for building, testing, and deployment, eliminating modularization’s benefits. Extract shared types downward, invert dependency with a protocol (DIP), or connect both modules in an upper assembly layer to make the arrows one-way.
Q. Explain the Stable Dependencies Principle (SDP).
A module should depend only on modules more stable than itself—those that change less. If frequently changing modules sit downstream, changes propagate throughout the upstream system, so low-change domain and interface modules belong at the bottom of a healthy graph.
The next article covers a trap many teams fall into during modularization: the dangerously named Common module.
We’ll examine how “it’s shared, so let’s put it in Common” turns the entire module system back into one unit, and how layered architecture can prevent it.

![Cover image for [Modularization #2] Module Dependencies: Designing and Breaking Cycles](/assets/images/posts/43006932-2be1-4618-af56-bd9ceee6264e/1.jpg)