Advanced Rust Design Patterns

Common Anti-Patterns

Avoiding common Rust anti-patterns prevents borrow-checker friction, hidden heap allocations, fragile panic paths, and confusing method resolution behavior.

Top 6 Rust Anti-Patterns & Their Idiomatic Replacements:

  1. Over-Concrete Borrowing: `&String`, `&Vec<T>`, `&PathBuf` ---> Use `&str`, `&[T]`, `&Path`!
  2. `Deref` Polymorphism Abuse (Emulating OOP Inheritance)    ---> Use explicit composition or traits!
  3. Clone-to-Satisfy-the-Borrow-Checker                       ---> Restructure borrow scopes or use `Cow` / indices!
  4. Boolean Trap Parameters (`connect("host", true, false)`)  ---> Use two-variant `enum`s or a Builder!
  5. Stringly-Typed Errors (`Result<T, String>`)               ---> Use structured `enum` implementing `Error`!
  6. Sentinel Values (`-1` or `""` for missing data)           ---> Use `Option<T>` (`NonZero*` niche)!

1. Over-Concrete Reference Parameters (&String, &Vec<T>, &PathBuf)

Never write fn process(name: &String) or fn sum(items: &Vec<i32>)! Because &String automatically coerces to &str and &Vec<T> coerces to &[T] via Deref, accepting &str, &[T], and &Path allows callers to pass both heap-allocated owners (&my_string, &my_vec) and zero-allocation string/array literals ("literal", &[1, 2, 3]) or sub-slices (&my_vec[2..5])!

2. Abusing Deref to Simulate OOP Class Inheritance

Programmers coming from C++ or Java sometimes implement Deref<Target = BaseStruct> on DerivedStruct so DerivedStruct "inherits" all methods of BaseStruct.

This is a well-known Rust anti-pattern:

  • Deref is intended exclusively for smart pointers and transparent views (Box<T>, Arc<T>, MutexGuard<T>, String -> str, Vec<T> -> [T]) where the wrapper is a pointer to the target.
  • Implementing Deref on domain structs (such as Employee dereferencing to Person) breaks generic trait bounds (Employee still does not implement traits implemented by Person!), causes confusing PartialEq/Hash discrepancies, and hides which struct's method is being called. Use explicit accessor methods (emp.person()) or shared traits instead!

3. Replacing "Boolean Parameter Traps" with Self-Documenting Enums

When a function takes one or more bool parameters (file_copy(src, dst, true, false)), a reader at the call site cannot tell what true and false mean without looking up the function definition—and passing (false, true) by mistake compiles without warning!

Replacing boolean flags with 2-variant Copy enums makes call sites self-documenting and impossible to transpose:

// ❌ ANTI-PATTERN: Call site `sync_directory(src, dst, true, false)` is unreadable!
// pub fn sync_directory_bad(src: &String, dst: &String, recursive: bool, overwrite: bool)

// ✅ IDIOMATIC RUST: Accepts `&str` slices and self-documenting 2-variant enums!
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraversalMode {
    Shallow,
    Recursive,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConflictPolicy {
    SkipExisting,
    OverwriteExisting,
}

pub fn sync_directory(
    src: &str,
    dst: &str,
    mode: TraversalMode,
    policy: ConflictPolicy,
) -> String {
    format!("Sync '{src}' -> '{dst}' ({mode:?}, {policy:?})")
}

fn main() {
    // Call site is 100% self-documenting and compiler-checked against argument transposition:
    let summary = sync_directory(
        "/var/data",
        "/mnt/backup",
        TraversalMode::Recursive,
        ConflictPolicy::SkipExisting,
    );

    println!("{summary}");
}
Sync '/var/data' -> '/mnt/backup' (Recursive, SkipExisting)

Summary Table of Anti-Patterns vs Idiomatic Rust

Anti-Pattern Problem Caused Idiomatic Replacement
fn foo(v: &Vec<T>, s: &String) Forces callers with &[T] or &str literals to allocate on the heap just to call foo. fn foo(v: &[T], s: &str)
if opt.is_some() { let x = opt.unwrap(); } Redundant double check; risks panic if refactored when condition changes. if let Some(x) = opt { ... } or let Some(x) = opt else { return; };
impl Deref<Target = Base> for Sub Does not satisfy trait bounds T: BaseTrait; confuses method resolution. Explicit composition (sub.base()) or shared trait implementation.
vec.iter().map(...).collect::<Vec<_>>() (when only iterating once) Unnecessary intermediate heap allocation. Keep the iterator lazy until the final consumer!
Returning -1 or usize::MAX for "not found" Caller can forget to check the magic sentinel and use it as a real index/count. Return Option<usize> (or Option<NonZeroUsize> for 8-byte niche size!).