Interior Mutability

Choosing the Appropriate Primitive

Rust provides a specialized suite of interior mutability primitives across std::cell and std::sync so you never pay for synchronization, atomic instructions, or runtime borrow counters that your access pattern does not require.

Decision Tree for Interior Mutability Primitives

Need to mutate state through a shared reference (`&T`)?
  |
  +-- 1. Is the value accessed across multiple threads (`Sync` required)?
  |     |
  |     +-- NO (Single-Threaded -> `std::cell`)
  |     |     +-- Initialized once, then read-only `&T`?
  |     |     |     +-- Initializer fixed at creation ---------> `LazyCell<T>`
  |     |     |     +-- Initialized on demand / with args -----> `OnceCell<T>`
  |     |     +-- Mutated repeatedly by value (`Copy`, `Default`, or swap)?
  |     |     |     +------------------------------------------> `Cell<T>`
  |     |     +-- Need `&T` and `&mut T` references to inner value?
  |     |           +------------------------------------------> `RefCell<T>`
  |     |
  |     +-- YES (Multi-Threaded -> `std::sync` / `std::sync::atomic`)
  |           +-- Initialized once, then read-only `&T`?
  |           |     +-- Initializer fixed (`static` global) ---> `LazyLock<T>`
  |           |     +-- Initialized on demand / with args -----> `OnceLock<T>`
  |           +-- Single primitive integer / boolean / pointer?
  |           |     +------------------------------------------> `AtomicBool` / `AtomicUsize` / `AtomicU64`
  |           +-- Arbitrary struct / collection mutated repeatedly?
  |                 +-- General exclusive read/write access ---> `Mutex<T>`
  |                 +-- High read concurrency, infrequent writes -> `RwLock<T>`

Complete Reference Table

Primitive Module Thread Safe (Sync) Returns &T Directly? Returns Guard (Deref)? Can Panic or Deadlock on Access? Extra Size Overhead (64-bit)
Cell<T> std::cell No No (copies/moves T) No Never 0 bytes
RefCell<T> std::cell No No Ref<T> / RefMut<T> Panics on conflicting borrow 8 bytes (isize)
OnceCell<T> std::cell No Yes (Option<&T>) No Panics only on re-entrant get_or_init Discriminant of Option<T>
LazyCell<T, F> std::cell No Yes (via Deref) No (LazyCell itself derefs) Panics only on re-entrant init Size of F / discriminant
AtomicU64 / AtomicBool std::sync::atomic Yes No (loads/stores value) No Never (lock-free hardware ops) 0 bytes
OnceLock<T> std::sync Yes Yes (Option<&T>) No Deadlocks only on recursive get_or_init Once state (4–8 bytes) + Option<T>
LazyLock<T, F> std::sync Yes Yes (via Deref) No (LazyLock itself derefs) Deadlocks/panics only on recursive init Once state + size of F
Mutex<T> std::sync Yes (T: Send) No MutexGuard<'_, T> Can deadlock if relocked on same thread; poisons on panic OS mutex header (8–40 bytes)
RwLock<T> std::sync Yes (T: Send + Sync) No RwLockReadGuard / RwLockWriteGuard Can deadlock if relocked on same thread; poisons on write panic OS rwlock header (8–56 bytes)

Combining Multiple Primitives in One Service Struct

Real-world types frequently combine several interior mutability primitives—choosing the lightest primitive for each individual field's access pattern:

use std::cell::{Cell, OnceCell, RefCell};

struct HttpClient {
    // 1. Write-once lazy TLS root certificate store -> `OnceCell` (returns `&str` with no guard):
    user_agent: OnceCell<String>,
    // 2. Simple `Copy` counter -> `Cell` (0 bytes overhead, never panics):
    total_requests: Cell<u64>,
    // 3. Growable collection borrowed by reference -> `RefCell`:
    recent_urls: RefCell<Vec<String>>,
}

impl HttpClient {
    fn new() -> Self {
        Self {
            user_agent: OnceCell::new(),
            total_requests: Cell::new(0),
            recent_urls: RefCell::new(Vec::new()),
        }
    }

    fn agent(&self) -> &str {
        self.user_agent
            .get_or_init(|| String::from("rust-http-client/1.0"))
    }

    fn get(&self, url: &str) {
        self.total_requests.set(self.total_requests.get() + 1);
        self.recent_urls.borrow_mut().push(url.to_string());
        println!("[GET #{}] {} (UA: {})", self.total_requests.get(), url, self.agent());
    }
}

fn main() {
    let client = HttpClient::new();
    client.get("https://example.com/status");
    client.get("https://example.com/metrics");

    println!("recent_urls: {:?}", client.recent_urls.borrow());
}
[GET #1] https://example.com/status (UA: rust-http-client/1.0)
[GET #2] https://example.com/metrics (UA: rust-http-client/1.0)
recent_urls: ["https://example.com/status", "https://example.com/metrics"]