Advanced Rust Design Patterns

Interior Mutability Pattern

The Interior Mutability pattern allows mutating a specific internal field—such as a lazy cache, reference count, metrics counter, or I/O buffer—behind a shared immutable reference (&self). Instead of forcing every caller in the program to hold an exclusive &mut Client just to increment a hit counter or populate a memoization cache, interior mutability moves the aliasing check from the whole struct at compile time to a narrow, encapsulated field.

Architectural Rule for Interior Mutability:
  Logically Immutable to the Caller (`&self` API)
       |
       +-- Physically Mutates a Private Cache / Counter Field Internally!
           - Single-threaded `Copy` field:  `Cell<T>`        (0 runtime borrow checks!)
           - Single-threaded heap/ref type: `RefCell<T>`     (Runtime borrow flag)
           - Single/Multi-threaded 1-time:  `OnceLock<T>` / `OnceCell<T>`
           - Multi-threaded counter/flag:   `AtomicU64` / `AtomicBool`
           - Multi-threaded complex state:  `Mutex<T>` / `RwLock<T>`

Choosing the Minimal Interior Mutability Primitive

Primitive Thread-Safe (Sync)? Can Panic / Deadlock? Cost Ideal Architectural Pattern
Cell<T> No (!Sync) Never! (No references into Cell can exist) 0 cycles (Direct get/set load/store) Mutating Copy flags, cursors, or counters inside !Sync types.
OnceCell<T> / LazyCell<T> No (!Sync) Never (unless recursive init) 1 branch check Lazy initialization of expensive single-threaded fields on first &self read.
RefCell<T> No (!Sync) Panics if .borrow_mut() overlaps .borrow() 1 integer increment/decrement Mutating graphs/observers in single-threaded UI or compiler passes.
Atomic* Yes (Sync) Never! 1 atomic CPU instruction Lock-free telemetry counters, sequence IDs, and cancellation flags on &self.
OnceLock<T> / LazyLock<T> Yes (Sync) Blocks concurrent init threads once 1 atomic load after init Thread-safe lazy initialization of compiled regexes, schemas, or cached fields.
Mutex<T> / RwLock<T> Yes (Sync) Deadlocks if locked twice on the same thread! ~15ns uncontended atomic lock Protecting shared connection pools or LRU caches inside a Sync service.

Pattern 1: Lazy Memoization on &self with OnceLock<T>

When a struct holds raw data and has an expensive derived property (such as a SHA-256 digest or parsed AST) that may or may not be requested by callers, storing cached_hash: OnceLock<u64> inside the struct allows fn hash(&self) -> u64 to take a shared &self reference, compute the hash on the first call, and return the cached value in $O(1)$ on all subsequent calls—even across multiple concurrent threads!

use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};

pub struct Document {
    content: String,
    // Lazily computed at most once, even if multiple threads call `.digest()` on `&Document`!
    cached_digest: OnceLock<u64>,
    // Lock-free access counter updated via `&self`:
    read_count: AtomicU64,
}

impl Document {
    pub fn new(content: impl Into<String>) -> Self {
        Self {
            content: content.into(),
            cached_digest: OnceLock::new(),
            read_count: AtomicU64::new(0),
        }
    }

    pub fn text(&self) -> &str {
        self.read_count.fetch_add(1, Ordering::Relaxed);
        &self.content
    }

    pub fn digest(&self) -> u64 {
        *self.cached_digest.get_or_init(|| {
            // Expensive computation runs ONLY on the first call!
            self.content
                .bytes()
                .fold(0xcbf29ce484222325u64, |acc, b| {
                    (acc ^ u64::from(b)).wrapping_mul(0x100000001b3)
                })
        })
    }

    pub fn reads(&self) -> u64 {
        self.read_count.load(Ordering::Relaxed)
    }
}

fn main() {
    // Notice `doc` is NOT declared `mut` (`let doc`, not `let mut doc`)!
    let doc = Document::new("immutable-on-the-outside");

    assert_eq!(doc.text(), "immutable-on-the-outside");
    assert_eq!(doc.text(), "immutable-on-the-outside");
    assert_eq!(doc.reads(), 2);

    let d1 = doc.digest();
    let d2 = doc.digest(); // Returns cached value from `OnceLock` immediately!
    assert_eq!(d1, d2);

    println!("Document reads={}, cached digest={d1:#x}", doc.reads());
}
Document reads=2, cached digest=0xd9924c89069bc1c8

Golden Rule for RefCell and Mutex Scope

Never return a Ref<'_, T> or MutexGuard<'_, T> across a public API boundary if you can avoid it, and never hold a RefCell borrow or Mutex lock across an external callback or .await point! Keep the .borrow_mut() or .lock() confined to a 2–3 line private block so callers cannot trigger a reentrant borrow panic or deadlock.