From e1c2caf8d540683d9e6afbe5201196230eea1632 Mon Sep 17 00:00:00 2001 From: Rodin Date: Thu, 30 Apr 2026 15:11:43 -0700 Subject: [PATCH] docs: module organization patterns from rust-lang/rust 10 patterns, 514 lines. Full spec compliance. Patterns: one type per file, pub use re-exports, prelude module, feature gates, API/impl separation, test organization, type aliases, workspace multi-crate, conditional compilation, error modules. --- patterns/module-organization.md | 514 ++++++++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 patterns/module-organization.md diff --git a/patterns/module-organization.md b/patterns/module-organization.md new file mode 100644 index 0000000..10014ce --- /dev/null +++ b/patterns/module-organization.md @@ -0,0 +1,514 @@ +# Rust Module Organization Patterns + +Patterns for organizing code into modules, extracted from the +standard library source. + +**Source:** [rust-lang/rust](https://github.com/rust-lang/rust) at commit +[`f53b654`](https://github.com/rust-lang/rust/tree/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6) + +**Stats:** 220 mod.rs files, 41 lib.rs files, 1,404 module +declarations, 919 pub(crate) items, 18,430 pub fn. + +--- + +## 1. One Type Per File (Large Types) + +### Source: + +[library/alloc/src/string.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/string.rs) (String), [library/alloc/src/vec/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/alloc/src/vec/mod.rs) (Vec) + +```text +alloc/src/ +├── string.rs ← String type + all impls (3000+ lines) +├── vec/ +│ ├── mod.rs ← Vec type + core methods +│ ├── drain.rs ← Drain iterator +│ ├── into_iter.rs ← IntoIter +│ ├── splice.rs ← Splice iterator +│ └── ... +└── collections/ + ├── btree/ + │ ├── map.rs + │ ├── set.rs + │ └── ... + └── ... +``` + +### Why + +Large types get their own file (or directory). This keeps files +navigable. When a type has many associated iterators or helpers, +it becomes a directory with `mod.rs`. The split is by related type, +not by "kind" (don't put all iterators in `iterators.rs`). + +### When to Use + +**Triggers:** +- A type + impls exceeds ~500 lines +- A type has multiple associated types (iterators, builders) +- Multiple people work on the same module (reduce conflicts) + +**Example — before:** +```rust +// src/collections.rs — 5000 lines, everything jammed together +pub struct HashMap { ... } +impl HashMap { /* 800 lines */ } +pub struct HashSet { ... } +impl HashSet { /* 600 lines */ } +pub struct BTreeMap { ... } +// Impossible to navigate +``` + +**Example — after:** +```rust +// src/collections/mod.rs +pub mod hash_map; +pub mod hash_set; +pub mod btree_map; + +// src/collections/hash_map.rs +pub struct HashMap { ... } +impl HashMap { ... } +``` + +### When NOT to Use + +**Don't use this when:** +- The type is small (<100 lines with impls) +- Related types are tightly coupled and small (keep together) + +--- + +## 2. Re-exports for Clean Public API (pub use) + +### Source: + +[library/std/src/lib.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/lib.rs) + +```rust +// Internal organization: +mod io; +mod fs; +mod net; + +// Public API — re-export the important bits: +pub use io::{Read, Write, BufRead, Seek}; +pub use fs::{File, OpenOptions}; +``` + +### Why + +Internal module structure serves implementation needs. Public API +structure serves user needs. They're often different. `pub use` +lets you organize internally however makes sense while presenting +a clean, flat API to users. + +### When to Use + +**Triggers:** +- Internal modules are deeply nested but users want flat access +- You want `use my_crate::ImportantType` instead of + `use my_crate::internal::deep::nested::ImportantType` +- Reorganizing internals without breaking the public API + +**Example — before:** +```rust +// Users must know internal structure: +use my_crate::network::http::client::HttpClient; +use my_crate::network::http::request::Request; +use my_crate::network::http::response::Response; +``` + +**Example — after:** +```rust +// lib.rs re-exports at the crate root: +pub use network::http::client::HttpClient; +pub use network::http::request::Request; +pub use network::http::response::Response; + +// Users just write: +use my_crate::{HttpClient, Request, Response}; +``` + +### When NOT to Use + +**Don't use this when:** +- The module hierarchy IS the API (users should know about it) +- Re-exports would cause name collisions +- You're re-exporting too much (defeats the purpose of modules) + +--- + +## 3. Prelude Module for Common Imports + +### Source: + +[library/std/src/prelude/mod.rs](https://github.com/rust-lang/rust/blob/f53b654a8882fd5fc036c4ca7a4ff41ce32497a6/library/std/src/prelude/mod.rs) + +```rust +pub mod prelude { + pub mod v1 { + pub use crate::option::Option::{self, Some, None}; + pub use crate::result::Result::{self, Ok, Err}; + pub use crate::iter::Iterator; + pub use crate::clone::Clone; + // ... most commonly used traits and types + } +} +``` + +### Why + +A prelude gathers the most commonly used items so users can do +`use my_crate::prelude::*` instead of 20 individual imports. The +stdlib prelude is auto-imported; library preludes are opt-in. + +### When to Use + +**Triggers:** +- Your library has 5+ types/traits that users import together +- Most users need the same set of imports +- Reducing boilerplate matters for ergonomics + +**Example — before:** +```rust +// Every file in the project: +use my_framework::{App, Context, Request, Response, Handler}; +use my_framework::middleware::{Logger, Cors, Auth}; +use my_framework::error::Error; +// 3 lines of imports every single time +``` + +**Example — after:** +```rust +// my_framework/src/prelude.rs +pub use crate::{App, Context, Request, Response, Handler}; +pub use crate::middleware::{Logger, Cors, Auth}; +pub use crate::error::Error; + +// Every file in the project: +use my_framework::prelude::*; +``` + +### When NOT to Use + +**Don't use this when:** +- The library is small (few imports anyway) +- Items would conflict with common names (String, Error) +- You want explicit imports for clarity + +--- + +## 4. Feature Gates (#[cfg(feature = "...")]) + +### Source: + +The stdlib uses extensive feature gating for unstable features. +Libraries use it for optional dependencies. + +```rust +#[cfg(feature = "serde")] +impl Serialize for MyType { ... } + +#[cfg(feature = "async")] +pub mod async_client { ... } +``` + +### Why + +Feature gates let you provide optional functionality without +imposing dependencies on all users. Users opt in via Cargo.toml. +This keeps the default compilation fast and the dependency tree +small. + +### When to Use + +**Triggers:** +- Optional dependencies (serde, tokio, etc.) +- Optional functionality that not all users need +- Platform-specific code +- Unstable/experimental APIs + +**Example — before:** +```rust +// Everyone pays the compile cost of serde even if they don't use it +use serde::{Serialize, Deserialize}; + +#[derive(Serialize, Deserialize)] +pub struct Config { ... } +``` + +**Example — after:** +```rust +// Cargo.toml: [features] serde = ["dep:serde"] +#[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Config { ... } +// Only compiled with serde when the feature is enabled +``` + +### When NOT to Use + +**Don't use this when:** +- The functionality is core (everyone needs it) +- Feature interaction is complex (too many combinations to test) +- The dependency is tiny (not worth the complexity) + +--- + +## 5. Separating Public API from Implementation + +### Source: + +```text +std/src/io/ +├── mod.rs ← public API, re-exports, module docs +├── buffered/ ← implementation details +│ ├── bufreader.rs +│ ├── bufwriter.rs +│ └── linewriter.rs +├── cursor.rs ← Cursor implementation +├── error.rs ← Error types +└── impls.rs ← trait implementations +``` + +### Why + +The `mod.rs` (or parent module) defines WHAT users see. Sub-modules +contain HOW things work. This separation means you can refactor +internals without changing the public API. + +### When to Use + +**Triggers:** +- Module has both public API and complex implementation +- You want to reorganize implementation without breaking users +- The implementation has multiple sub-components + +### When NOT to Use + +**Don't use this when:** +- The module is simple (one file is fine) +- Implementation IS the API (no separation needed) + +--- + +## 6. tests/ Subdirectory for Test Organization + +### Source: + +```text +library/ +├── core/ +│ └── src/ +│ └── option.rs ← implementation +├── coretests/ +│ └── tests/ +│ └── option.rs ← tests for core::option +└── alloctests/ + └── tests/ + └── vec.rs ← tests for alloc::vec +``` + +### Why + +The stdlib separates tests into their own crates (`coretests`, +`alloctests`) because `core` can't depend on `alloc` for testing. +For normal projects, `tests/` directory provides the same separation +for integration tests. + +### When to Use + +**Triggers:** +- Integration tests (testing the public API) +- Tests that need external resources +- Tests that should be separate binaries (parallelism) + +--- + +## 7. Type Aliases for Complex Generics + +### Source: + +```rust +// Instead of writing Result everywhere: +pub type Result = std::result::Result; + +// Instead of writing Box Response + Send + Sync>: +pub type Handler = Box Response + Send + Sync>; +``` + +### Why + +Type aliases reduce visual noise for complex generic types. They +make function signatures readable and provide a single point of +change if the underlying type evolves. + +### When to Use + +**Triggers:** +- A generic type appears in 3+ signatures +- The full type is > 40 characters +- You want a domain-specific name for a generic composition + +### When NOT to Use + +**Don't use this when:** +- The alias hides important information (what IS that thing?) +- The alias is used in only one place +- The original type is already short and clear + +--- + +## 8. Workspace Organization (Multi-Crate Projects) + +### Source: + +The Rust stdlib itself is organized as multiple crates: + +```text +library/ +├── core/ ← no allocator, no OS (can run on bare metal) +├── alloc/ ← needs an allocator (Vec, String, Box) +├── std/ ← needs an OS (files, threads, networking) +└── proc_macro/ ← procedural macro support +``` + +### Why + +Multiple crates within one workspace provide: +- Separate compilation (parallel builds) +- Clear dependency boundaries (core doesn't depend on alloc) +- Different feature sets per crate +- Independent versioning + +### When to Use + +**Triggers:** +- Different parts of your project have different dependencies +- You want parallel compilation (workspace members build in parallel) +- Some code is reusable independently (separate crate = separate publish) +- Clear architectural boundaries + +### When NOT to Use + +**Don't use this when:** +- The project is small (<2000 lines) +- Everything depends on everything else (no natural boundaries) +- The complexity of multiple crates outweighs the benefits + +--- + +## 9. Conditional Compilation (#[cfg]) + +### Source: + +Extensive use of `#[cfg(target_os)]`, `#[cfg(feature)]`, +`#[cfg(test)]` throughout the stdlib. + +```rust +#[cfg(unix)] +mod unix_impl; +#[cfg(windows)] +mod windows_impl; + +#[cfg(target_pointer_width = "64")] +type Repr = repr_bitpacked::Repr; +#[cfg(not(target_pointer_width = "64"))] +type Repr = repr_unpacked::Repr; +``` + +### Why + +`#[cfg]` compiles code conditionally. Dead code is eliminated at +compile time — no runtime cost. This is how one codebase supports +multiple platforms, feature sets, and configurations. + +### When to Use + +**Triggers:** +- Platform-specific implementations (OS, architecture) +- Feature-gated code +- Test-only code (`#[cfg(test)]`) +- Debug-only code (`#[cfg(debug_assertions)]`) + +### When NOT to Use + +**Don't use this when:** +- Runtime feature detection is needed (cfg is compile-time only) +- The conditional logic is simple enough for `if cfg!(...)` macro +- You're using cfg to hide broken code (fix it instead) + +--- + +## 10. Error Modules + +### Source: + +```text +std/src/io/ +├── error.rs ← Error type definition +├── error/ +│ ├── repr_bitpacked.rs ← compact representation +│ └── repr_unpacked.rs ← standard representation +└── mod.rs ← re-exports Error +``` + +### Why + +Error types often need their own module when they have: +- Multiple representations +- Many From implementations +- Complex display logic +- Associated helper types + +### When to Use + +**Triggers:** +- Error type exceeds 100 lines +- Error has multiple variants with different data +- Error needs platform-specific representations + +--- + +## Summary: Module Organization Decision Tree + +``` +How big is the module? +├── <200 lines → single file +├── 200-1000 lines → file with sections +└── >1000 lines → directory (mod.rs + sub-modules) + +What should users see? +├── Everything → pub mod (expose structure) +├── Flat API → pub use (re-export from depth) +└── Common subset → prelude module + +Platform-specific code? +├── cfg(unix) / cfg(windows) → parallel mod files +└── cfg(feature) → optional modules + +Test organization? +├── Unit tests → #[cfg(test)] mod tests (same file) +├── Integration → tests/ directory +└── Shared helpers → tests/common/mod.rs +``` + +| Pattern | Use when | +|---|---| +| One type per file | Large types (>500 lines with impls) | +| `pub use` re-exports | Clean API surface hiding internal structure | +| Prelude module | 5+ commonly imported items | +| Feature gates | Optional functionality/dependencies | +| API/impl separation | Public surface ≠ implementation structure | +| `tests/` directory | Integration testing | +| Type aliases | Complex generics appearing multiple times | +| Workspace (multi-crate) | Clear architectural boundaries | +| `#[cfg]` | Platform/feature conditional compilation | +| Error modules | Complex error types | + +See also: +- [api-design.md](api-design.md) — Public API design patterns +- [documentation.md](documentation.md) — Module-level documentation + +