How to Handle Database Transactions in Rust

Use a transaction manager trait from your specific database driver (like `sqlx`, `diesel`, or `tokio-postgres`) to wrap database operations in a single atomic block, ensuring that either all changes commit or none do if an error occurs.

When one owner isn't enough

Rust's ownership rule says every value has exactly one owner. The owner is responsible for cleaning the value up when its scope ends. That rule keeps the language safe and fast. It also occasionally gets in your way.

Picture a graph. Several nodes need to know about a shared list of categories. Or a parser where multiple syntax tree branches all reference the same source string. Or a UI where a piece of state is read by three different views. There's no single "main" owner. Several places need to be able to read the same data, and the data should stay alive as long as anyone is still using it.

That's what Rc<T> is for. The name stands for "reference counted." Think of it like a library card on the inside cover of a book: every time someone borrows the book, the librarian writes a tally mark. When someone returns the book, the librarian crosses one out. When the last mark gets crossed off, the book goes back on the shelf and the storage is reclaimed.

How it actually works

Rc::new(value) puts your value on the heap, alongside a counter that starts at one. When you call Rc::clone, you don't copy the value. You bump the counter and hand back another Rc pointing at the same heap allocation. Each Rc that goes out of scope decrements the counter. When the counter hits zero, the value gets dropped and the memory is freed.

use std::rc::Rc;

fn main() {
    // The String goes on the heap. The Rc wrapper holds a pointer plus a count.
    let data = Rc::new(String::from("Shared state"));

    // Clone the Rc, not the String. Counter goes from 1 to 2.
    let clone1 = Rc::clone(&data);

    // Another clone. Counter is now 3.
    let clone2 = Rc::clone(&data);

    // Check the count. This is a debug helper, not for production logic.
    println!("Strong count: {}", Rc::strong_count(&data)); // Prints 3

    // `clone2` drops here. Counter goes to 2.
}
// `clone1` and `data` drop here. Counter hits 0. String is freed.

The counter lives on the heap too, right next to your data. This keeps the memory footprint small. You're not allocating a new box for every clone. You're just copying a pointer and incrementing an integer. The integer is atomic in Arc<T>, but in Rc<T> it's a plain integer. That means Rc<T> is not thread-safe. You can't send an Rc across threads. If you try, the compiler rejects you with E0277 (the trait bound Send is not satisfied).

The convention: explicit clone

You'll see two ways to clone an Rc in the wild:

let a = Rc::new(vec![1, 2, 3]);
let b = a.clone();
let c = Rc::clone(&a);

Both compile. Both do the exact same thing. The community convention is Rc::clone(&a). The reason is readability. When you see a.clone(), you might assume it's a deep clone that copies the vector. Rc::clone(&a) makes it obvious you're cloning the reference, not the data. It signals intent. Follow the convention. It saves future-you from a moment of confusion.

When you need mutation

Rc<T> gives you shared ownership, but it doesn't give you mutation. If you try to mutate the value through an Rc, the compiler stops you. You can't have multiple owners and mutable access at the same time. That would break Rust's aliasing rules.

To mutate shared data, you need interior mutability. Combine Rc with RefCell. RefCell<T> enforces the borrow rules at runtime instead of compile time. You can borrow the value mutably as long as no one else is borrowing it. If you violate the rules, you get a panic, not a compile error.

use std::rc::Rc;
use std::cell::RefCell;

#[derive(Debug)]
struct Node {
    value: i32,
    children: RefCell<Vec<Rc<RefCell<Node>>>>,
}

fn main() {
    let node = Rc::new(RefCell::new(Node {
        value: 1,
        children: RefCell::new(vec![]),
    }));

    // Borrow the node mutably to add a child.
    // `borrow_mut` checks the runtime counter. Panics if already borrowed.
    node.borrow_mut().children.borrow_mut().push(
        Rc::new(RefCell::new(Node {
            value: 2,
            children: RefCell::new(vec![]),
        }))
    );

    println!("{:?}", node);
}

This pattern is common in GUIs and interpreters where the structure is built dynamically and shared across callbacks. The runtime check is the price you pay for flexibility. The borrow checker can't verify the borrows at compile time because the ownership graph is too complex. RefCell steps in and checks at runtime.

Cycles and memory leaks

Reference counting has a flaw. Cycles leak memory. If object A holds an Rc to object B, and object B holds an Rc to object A, the counters never hit zero. A points to B, so B stays alive. B points to A, so A stays alive. Neither drops. The memory is lost for the lifetime of the program.

Rust gives you Weak<T> to break cycles. A Weak reference points to the same allocation as an Rc, but it doesn't increment the strong count. It increments a separate weak count. The value is dropped when the strong count hits zero, even if weak references exist. You can upgrade a Weak to an Rc if the value is still alive. If it's gone, the upgrade returns None.

use std::rc::{Rc, Weak};

#[derive(Debug)]
struct Parent {
    children: Vec<Rc<Child>>,
}

#[derive(Debug)]
struct Child {
    // Use Weak to avoid a cycle. The child doesn't own the parent.
    parent: Weak<Parent>,
}

fn main() {
    let parent = Rc::new(Parent { children: vec![] });

    let child = Rc::new(Child {
        parent: Rc::downgrade(&parent),
    });

    // Add child to parent.
    parent.children.push(child);

    // Access parent from child.
    if let Some(p) = parent.children[0].parent.upgrade() {
        println!("Parent has {} children", p.children.len());
    }
}

Use Weak for back-references. A child knows its parent, but the parent owns the child. The parent's Rc keeps the child alive. The child's Weak points back without creating a cycle. This is the standard pattern for trees and graphs.

Pitfalls and gotchas

Cloning an Rc is cheap, but not free. It involves a heap allocation for the counter and a pointer copy. If you're in a tight loop and cloning millions of times, the overhead adds up. Profile before optimizing. Usually, the allocation is negligible compared to the work you're doing.

Another pitfall is assuming Rc is thread-safe. It isn't. The counter is not atomic. Concurrent increments can corrupt the count. If you need shared ownership across threads, use Arc<T>. Arc stands for "atomic reference count." It uses atomic operations to update the counter. The operations are more expensive, but they're safe for concurrent access.

You might also run into E0502 (cannot borrow as mutable because it is also borrowed as immutable) when using RefCell. This happens if you try to borrow mutably while an immutable borrow exists. The borrow checker catches this at compile time for regular references. RefCell catches it at runtime. If you hold a borrow() and then call borrow_mut(), the program panics. Be careful with nested borrows. Drop the immutable borrow before requesting the mutable one.

Decision matrix

Use Rc<T> when you have shared ownership within a single thread and the data is immutable. This is the default choice for shared state in single-threaded contexts.

Use Rc<RefCell<T>> when you need shared ownership and mutation within a single thread. The runtime borrow checks are the trade-off for flexibility. Use this for GUIs, interpreters, or complex graphs where compile-time borrowing is too restrictive.

Use Arc<T> when you need shared ownership across threads. The atomic counter adds overhead, but it's the only safe way to share data between threads.

Use Arc<Mutex<T>> when you need shared ownership and mutation across threads. Mutex provides interior mutability with blocking synchronization. The lock ensures only one thread mutates the data at a time.

Use Weak<T> to break cycles in graphs and trees. Back-references should always be weak. The owner holds strong references. Dependents hold weak references.

Use plain references when lifetimes are simple. Rc is rarely worth it if you can express the relationship with borrows. Reach for Rc only when the borrow checker forces you to.

Where to go next