Interior Mutability Patterns
Interior mutability lets you mutate data through shared references when runtime borrow rules or synchronization replace compile-time exclusivity.
Recipe
use std::cell::RefCell;
let cache = RefCell::new(HashMap::new());
cache.borrow_mut().insert("k", 1);When to reach for this: Graphs with aliasing, single-thread caches, or shared counters with Mutex/Atomic.
Working Example
use std::sync::{Arc, Mutex};
struct AppState {
counter: Mutex<u64>,
}
async fn handler(state: Arc<AppState>) {
let mut n = state.counter.lock().unwrap();
*n += 1;
}What this demonstrates:
RefCellfor single-thread interior mut (runtime borrow panic if violated)Mutexfor multi-thread shared mutationCellforCopytypes onlyArc<Mutex<T>>common in Axum shared state (prefertokio::sync::Mutexin async)
Deep Dive
| Type | Threads | Panic on conflict |
|---|---|---|
Cell | No | No (Copy only) |
RefCell | No | Yes (borrow) |
Mutex | Yes | Poison error |
RwLock | Yes | Poison error |
Atomic* | Yes | No |
Prefer message passing or owned mutation before Rc<RefCell<_>>.
Gotchas
- Rc<RefCell> everywhere - runtime panics and tangled graph. Fix: arena + indices or unique ownership.
- Mutex across await - Send issues and deadlocks. Fix:
tokio::sync::Mutex; lock scope minimal, no await while holding std Mutex. - unwrap on lock poison - hides bugs. Fix: map poison error or
lock().expect("msg")with policy. - RefCell in async task - not Sync. Fix: do not put RefCell in
Arcshared across threads. - Hidden mutation in &self method - surprising API. Fix: document thread-safety and mutation in method docs.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Owned mut | Single owner | Shared cache needed |
| Channels | Producer/consumer | Fine-grained shared state |
DashMap | Concurrent map | Simple single-thread |
FAQs
RefCell vs Mutex single-thread?
RefCell cheaper; no threads involved.
tokio Mutex vs std?
tokio Mutex await-friendly; std Mutex blocks executor if held across await.
interior mutability in trait?
&self methods mutating inner Mutex is standard pattern.
Cell for bool flags?
Yes for single-thread toggles.
OnceCell interior?
Lazy init without mut ref after init; different pattern.
graph with RefCell?
Possible for AST; consider arena if performance matters.
Send Sync RefCell?
RefCell not Sync; cannot share across threads in Arc without Mutex.
clippy mutex?
Warn on std Mutex in async contexts; use tokio mutex.
test borrow panic?
#[should_panic] on double borrow_mut in tests.
when atomics enough?
Counters and flags without protecting large structs.
Related
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.