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