When two modules wait on each other
You are building a game engine. The player module needs to know about Enemy so the player can attack. The enemy module needs to know about Player so the enemy can target the player. You write use crate::enemy::Enemy; inside player.rs. You write use crate::player::Player; inside enemy.rs. You hit compile. Rust screams cyclic dependency detected. You stare at the screen. The compiler refuses to load either file because each is waiting for the other to finish loading first. It is a deadlock of definitions.
The dependency graph needs a root
Rust builds your code by resolving dependencies in a strict order. If module A needs module B, Rust loads B first, then A. This creates a chain. A chain needs a beginning. A cycle has no beginning.
Think of a dictionary. You look up "happy". The definition says "feeling joy". You look up "joy". The definition says "feeling happy". You are stuck. You cannot understand either word because each definition relies on the other. Rust's compiler hits the same wall. It refuses to guess. It demands a clear hierarchy where every module eventually traces back to a root that depends on nothing else.
The compiler performs a topological sort on your module graph. It looks for a node with no incoming edges to start the build. A cycle means every node has an incoming edge. The sort fails. The build fails. Rust prevents this because partial initialization of types can break memory layout guarantees and lead to undefined behavior in safe code.
Break the loop. Extract the shared shape.
The minimal cycle
Here is the smallest possible cycle. Two modules try to use each other's types.
// This code fails to compile.
// Module `a` needs `b` to exist before it can define `ItemA`.
// Module `b` needs `a` to exist before it can define `ItemB`.
// Rust cannot resolve this loop.
mod a {
// We try to pull `ItemB` from module `b`.
// Rust must process `b` to understand this import.
use crate::b::ItemB;
/// ItemA holds a reference to an ItemB.
/// Rust needs the full definition of ItemB to calculate the size of ItemA.
pub struct ItemA {
pub b: ItemB,
}
}
mod b {
// We try to pull `ItemA` from module `a`.
// Rust must process `a` to understand this import.
use crate::a::ItemA;
/// ItemB holds a reference to an ItemA.
/// Rust needs the full definition of ItemA to calculate the size of ItemB.
pub struct ItemB {
pub a: ItemA,
}
}
fn main() {}
The compiler sees the circle and stops. You need to cut the wire.
How the compiler gets stuck
When the compiler encounters mod a, it starts parsing the file. It hits use crate::b::ItemB. To understand that import, Rust must have already processed mod b. It jumps to mod b. Inside mod b, it hits use crate::a::ItemA. Now Rust needs mod a. It realizes it is already trying to load mod a. The dependency graph has a circle.
Rust stops immediately. It does not try to be clever. It reports the cycle and refuses to proceed. This behavior protects you from subtle bugs where types are partially initialized or forward-declared in ways that break memory layout guarantees. Rust requires every type to have a known size and layout before it can be used in another type. A cycle makes that impossible.
Rust demands a hierarchy. Give it a root.
Breaking the cycle in real code
In real projects, cycles usually happen because two modules share a type or a trait. The fix is to extract that shared piece into a third module. Both original modules depend on the third module, but not on each other.
Imagine a game where Player and Enemy both need a Position struct. If Player is in player.rs and Enemy is in enemy.rs, and they both define Position, you have duplication. If Player imports Position from enemy.rs and Enemy imports Position from player.rs, you have a cycle.
The solution is shared.rs.
// lib.rs
// Declare the modules.
// The order here does not matter for compilation,
// but it helps readers understand the structure.
pub mod shared;
pub mod player;
pub mod enemy;
// shared.rs
// This module breaks the cycle.
// It contains types that both player and enemy need.
// Neither player nor enemy depends on the other.
/// Represents coordinates in the game world.
pub struct Position {
pub x: f32,
pub y: f32,
}
// player.rs
use crate::shared::Position;
/// The player character.
/// Depends only on shared types, not on enemy.
pub struct Player {
pub name: String,
pub pos: Position,
}
// enemy.rs
use crate::shared::Position;
/// An enemy in the game.
/// Depends only on shared types, not on player.
pub struct Enemy {
pub kind: String,
pub pos: Position,
}
Now player depends on shared. enemy depends on shared. shared depends on nothing. The graph is a tree. The compiler is happy.
Convention tip: When you extract types to a shared module, use pub use in the original modules to keep the API familiar. Users of player can still write player::Position if player.rs contains pub use crate::shared::Position;. This hides the refactoring from downstream code and keeps the public interface clean.
The third module is your escape hatch. If two modules fight, give them a common ground.
Pitfalls and false friends
The "God Module" trap is real. You create shared.rs to fix one cycle. Then you add another shared type. Then another. Soon shared.rs contains half your codebase. You have solved the cycle but created a maintenance nightmare. Every change to shared ripples through the entire project. Extract only what is truly shared. Keep modules focused. If a type belongs logically with Player, keep it in player.rs even if Enemy needs it. In that case, Enemy should depend on Player, not the other way around. Direction matters.
Another trap is trying to hide the cycle with Box. Some developers think pub struct A { b: Box<B> } breaks the cycle. It does not. Box changes how memory is allocated, but it does not change module dependencies. If module a imports B from module b, and module b imports A from module a, the cycle remains. The compiler still needs to resolve both modules to understand the types. Box only helps with infinite recursion in data structures, like a linked list where a node contains a pointer to the next node. It does not fix module cycles.
Don't use Box to paper over a design smell. Fix the structure.
Decision: how to restructure
Use a third shared module when two modules reference each other's types. Extract the common types, traits, or enums into a new module and have both original modules depend on it.
Use a trait object when one module needs to call methods on a type from another module without knowing the concrete type. Define the trait in a shared module, implement it in the specific modules, and pass &dyn Trait across boundaries. This breaks the dependency because the shared module only knows the trait, not the implementations.
Use a single module when the types are tightly coupled and belong together. If Player and Inventory always appear together, put them in player.rs. One module is better than two modules fighting. Consolidation eliminates the cycle by merging the nodes.
Use Box or Rc only to break infinite recursion in data structures, not to hide module cycles. If you need Box to make the compiler accept a cross-module reference, you still have a dependency cycle to fix. The wrapper does not change the import graph.
Structure follows data flow. If the flow loops, the structure is wrong.