Advanced Rust Design Patterns
RAII Pattern
Resource Acquisition Is Initialization (RAII)—also known as Scope-Bound Resource Management (SBRM)—ties the lifetime of an external or stateful resource (such as an open file descriptor, a locked mutex, a database transaction, or a temporary metric span) directly to the lifetime of a stack-allocated Rust struct implementing the Drop trait.
RAII Scope Guard Lifecycle:
{
let guard = TransactionGuard::begin(&db); // <--- Resource acquired!
guard.execute("INSERT ... ")?; // <--- Early `?` return or panic?
// `guard.drop()` rolls back automatically!
guard.commit()?; // <--- Consumes `guard`, disarming rollback!
}
Three Idiomatic RAII Patterns in Rust
| RAII Pattern | Standard Library Example | How It Works |
|---|---|---|
1. Exclusive Access Guard (Deref + Drop) |
MutexGuard<'a, T>, RwLockWriteGuard<'a, T>, RefMut<'a, T> |
Acquires lock on creation, exposes the protected data via Deref/DerefMut, and releases the lock automatically in Drop::drop. |
| 2. Automatic Rollback / Disarmable Guard | Database transactions, staged atomic file writes (TempFile::persist) |
Drop rolls back uncommitted changes on early ? return or panic; calling .commit(mut self) sets committed = true (or uses ManuallyDrop) to disarm the rollback! |
3. Scope Exit / Defer Guard (ScopeGuard) |
Restoring a state flag, logging elapsed timer duration | Runs a cleanup closure when the current lexical scope exits via normal return, ?, break, or panic unwinding. |
Implementing a Disarmable Rollback Guard (Transaction)
A classic systems design challenge is ensuring a multi-step operation is automatically rolled back if any intermediate step returns Err(...) via ? or panics, while committing cleanly if the caller reaches the end and calls .commit(self).
Because .commit(mut self) takes self by value, no caller can accidentally use the transaction after committing it:
#[derive(Debug, Default)]
pub struct Ledger {
pub balance: i64,
pub rollbacks: u32,
}
pub struct Transaction<'a> {
ledger: &'a mut Ledger,
initial_balance: i64,
committed: bool,
}
impl<'a> Transaction<'a> {
pub fn begin(ledger: &'a mut Ledger) -> Self {
let initial_balance = ledger.balance;
Self {
ledger,
initial_balance,
committed: false,
}
}
pub fn apply_delta(&mut self, delta: i64) -> Result<(), &'static str> {
let next = self.ledger.balance + delta;
if next < 0 {
return Err("overdraft rejected");
}
self.ledger.balance = next;
Ok(())
}
/// Consumes the transaction guard and marks it committed so `Drop` does not roll back!
pub fn commit(mut self) {
self.committed = true;
}
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
if !self.committed {
// Automatic rollback on early `?` return, unhandled error, or panic!
self.ledger.balance = self.initial_balance;
self.ledger.rollbacks += 1;
}
}
}
fn process_batch(ledger: &mut Ledger, deltas: &[i64]) -> Result<(), &'static str> {
let mut tx = Transaction::begin(ledger);
for &d in deltas {
tx.apply_delta(d)?; // If this `?` returns Err early, `tx` drops and rolls back!
}
tx.commit();
Ok(())
}
fn main() {
let mut ledger = Ledger {
balance: 100,
rollbacks: 0,
};
// 1. Successful batch commits changes:
process_batch(&mut ledger, &[50, -30]).unwrap();
assert_eq!(ledger.balance, 120);
assert_eq!(ledger.rollbacks, 0);
// 2. Failing batch (+80 succeeds, then -500 fails) rolls back the +80 automatically!
let res = process_batch(&mut ledger, &[80, -500]);
assert_eq!(res, Err("overdraft rejected"));
assert_eq!(ledger.balance, 120); // Restored to 120!
assert_eq!(ledger.rollbacks, 1);
println!(
"Ledger balance preserved after failed batch: {}, rollbacks: {}",
ledger.balance, ledger.rollbacks
);
}
Ledger balance preserved after failed batch: 120, rollbacks: 1