Modules and Visibility

Circular Dependency Considerations

Rust handles dependencies differently at the module level versus the crate level:

  1. Between modules inside the same crate: Cyclic imports (mod a using items from mod b while mod b uses items from mod a) are permitted by the compiler, because the entire crate is compiled as a single translation unit.
  2. Between separate Cargo crates: Cyclic dependencies (crate_a depending on crate_b while crate_b depends on crate_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, &notifier);
}
Email sent for Order #501 ($49.99)