Modules and Visibility
Circular Dependency Considerations
Rust handles dependencies differently at the module level versus the crate level:
- Between modules inside the same crate: Cyclic imports (
mod ausing items frommod bwhilemod buses items frommod a) are permitted by the compiler, because the entire crate is compiled as a single translation unit. - Between separate Cargo crates: Cyclic dependencies (
crate_adepending oncrate_bwhilecrate_bdepends oncrate_a) are strictly forbidden by Cargo; the crate dependency graph must be a Directed Acyclic Graph (DAG).
Intra-Crate Module Cycles vs. Inter-Crate Cycles
| Scope | Cyclic Dependency Allowed? | Architectural Guidance |
|---|---|---|
Modules within one crate (mod a <-> mod b) |
Yes (compiles cleanly) | Allowed for mutually recursive types (e.g., AST Expr <-> Stmt), but tangled business-logic module cycles should still be refactored into a layered hierarchy |
Crates in Cargo (crate_a <-> crate_b) |
No (cargo rejects cycles; except dev-dependencies for tests) |
Extract shared traits and domain types into a third foundation crate (crate_core) |
Breaking a Tangled Dependency Cycle Using Dependency Inversion (Shared Core / Trait):
Before (Cyclic Tangled Modules/Crates): After (Layered Acyclic Architecture):
+---------+ +---------+ +---------+ +---------+
| order | <=====> | payment | | order | | payment |
+---------+ +---------+ +----+----+ +----+----+
| |
+-------> + <-------+
|
+------v------+
| domain/core |
+-------------+
Valid Mutually Referencing Modules Inside a Crate
When two domain concepts naturally reference each other (such as an AST Expression that can contain a Block, and a Block that contains Expressions), placing them in sibling modules works directly:
mod expr {
use crate::stmt::Statement;
#[derive(Debug)]
pub enum Expr {
Literal(i32),
// References `Statement` from sibling `mod stmt` (via `Box` to have known size):
Block(Box<Statement>),
}
}
mod stmt {
use crate::expr::Expr;
#[derive(Debug)]
pub enum Statement {
// References `Expr` from sibling `mod expr`:
Return(Expr),
}
}
fn main() {
let ast = expr::Expr::Block(Box::new(stmt::Statement::Return(expr::Expr::Literal(42))));
println!("Constructed AST: {ast:?}");
}
Constructed AST: Block(Return(Literal(42)))
Breaking Architecture Cycles with Traits (Dependency Inversion)
When a high-level order_service module needs to send notifications via notifier, and notifier needs to read types or callbacks from order_service, define a trait in the lower/core module (or in order_service) and implement it in the outer adapter module:
mod core_types {
pub struct Order {
pub id: u64,
pub total_cents: u64,
}
// Trait defined in the core layer so `order_service` never imports `email_notifier`:
pub trait OrderNotifier {
fn notify_placed(&self, order: &Order);
}
}
mod order_service {
use crate::core_types::{Order, OrderNotifier};
pub fn place_order(id: u64, total_cents: u64, notifier: &impl OrderNotifier) -> Order {
let order = Order { id, total_cents };
notifier.notify_placed(&order);
order
}
}
mod email_notifier {
use crate::core_types::{Order, OrderNotifier};
pub struct SmtpNotifier;
impl OrderNotifier for SmtpNotifier {
fn notify_placed(&self, order: &Order) {
println!("Email sent for Order #{} (${:.2})", order.id, order.total_cents as f64 / 100.0);
}
}
}
fn main() {
let notifier = email_notifier::SmtpNotifier;
let _order = order_service::place_order(501, 49_99, ¬ifier);
}
Email sent for Order #501 ($49.99)