How to Use the Delegation Pattern in Rust

Implement the Deref trait in Rust to delegate method calls from a wrapper struct to its inner value.

When a wrapper needs to act like the thing inside

You are building a SmartCache<T> that logs every access. You want users to call cache.get() to trigger the log, but you also want cache to work anywhere a HashMap is expected. You could manually implement every method on HashMap, forwarding each call to the inner map. That works for five methods. It breaks down when the standard library adds a new method to HashMap in the next update, or when you realize you forgot to forward as_slice. You need a way to say, "This thing behaves exactly like the thing inside it, unless I explicitly override something."

That is the delegation pattern. In Rust, the compiler handles most of the heavy lifting through the Deref trait. You define a wrapper, implement Deref to point to the inner type, and the compiler automatically inserts the delegation calls. You get transparency without the boilerplate.

The concept: deref coercion

Deref stands for dereference. It is the trait behind the * operator. In C++, dereferencing a pointer gives you the value. In Rust, Deref does something more powerful. It powers deref coercion.

When the compiler sees a type mismatch where a reference is expected, it looks for a Deref implementation. If it finds one, it inserts a call to deref() to bridge the gap. This is how String works as str. You can pass a &String to a function expecting &str because String implements Deref<Target = str>. The compiler silently converts the reference.

You can use the same mechanism for custom types. The rule is simple: a type implements Deref to expose the inner value. The inner value is defined by the associated type Target. A type can only deref to one target. This constraint keeps the coercion chain predictable.

Think of Deref like a proxy card at a secure building. You hand the proxy card to the guard. The guard checks the card, sees the real ID inside, and lets you in. The card is the wrapper. The ID is the target. The guard is the compiler. The guard doesn't care about the card's design; it cares that the card reliably reveals the ID.

Minimal example

Here is the skeleton of a delegating wrapper. It wraps any type T and delegates all behavior to T.

use std::ops::Deref;

/// A generic wrapper that delegates to the inner value.
struct SmartBox<T>(T);

impl<T> SmartBox<T> {
    /// Create a new SmartBox.
    fn new(val: T) -> SmartBox<T> {
        SmartBox(val)
    }
}

impl<T> Deref for SmartBox<T> {
    type Target = T;

    /// Return a reference to the inner value.
    /// The compiler uses this to perform deref coercion.
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let b = SmartBox::new(5);
    
    // The compiler inserts .deref() here automatically.
    // It sees SmartBox<i32>, needs i32, and coerces via Deref.
    println!("Value: {}", b);
}

The type Target = T line tells the compiler what type lives inside. The deref method returns a reference to that type. Notice the return type is &Self::Target, not Self::Target. Deref never moves the value. It only borrows it. This preserves ownership rules. The wrapper still owns the data; it just lets others peek inside.

What the compiler does

When you write code that uses the wrapper, the compiler performs a search. Suppose you call len() on a SmartBox<Vec<i32>>.

  1. The compiler looks for a len method on SmartBox<Vec<i32>>. It finds none.
  2. It checks if SmartBox implements Deref. It does.
  3. It calls deref() to get a &Vec<i32>.
  4. It looks for len on Vec<i32>. It finds it.
  5. The call succeeds.

This happens at compile time. There is zero runtime cost for the coercion logic itself. The body of your deref method runs, but the decision to call it is baked into the binary.

Deref coercion can chain. If you have Rc<RefCell<Vec<i32>>>, the compiler can deref through Rc to RefCell, then through RefCell to Vec. You can call len() directly on the Rc. The compiler follows the chain automatically. The compiler limits the chain length to prevent infinite loops, usually capping it at three levels.

Explicit methods always win. If SmartBox implements its own len, that method is called. The compiler never derefs if a direct match exists. This lets you override specific behavior while delegating the rest.

Realistic wrapper with custom behavior

Delegation shines when you want to add a few custom methods while keeping the full API of the inner type. Here is a BoundedString that enforces a length limit at creation but acts like a String everywhere else.

use std::ops::Deref;

/// A String that enforces a maximum length at creation time.
/// After creation, it behaves exactly like a String.
struct BoundedString {
    inner: String,
    max_len: usize,
}

impl BoundedString {
    /// Create a BoundedString, panicking if the input exceeds max_len.
    fn new(s: &str, max_len: usize) -> BoundedString {
        assert!(s.len() <= max_len, "String too long");
        BoundedString {
            inner: s.to_string(),
            max_len,
        }
    }

    /// Custom method: check if we are near the limit.
    fn is_near_limit(&self) -> bool {
        self.inner.len() >= self.max_len - 5
    }
}

impl Deref for BoundedString {
    type Target = String;

    fn deref(&self) -> &Self::Target {
        &self.inner
    }
}

fn main() {
    let s = BoundedString::new("Hello", 10);
    
    // Custom method on the wrapper.
    println!("Near limit: {}", s.is_near_limit());
    
    // Delegated method from String.
    println!("Length: {}", s.len());
    
    // Delegated method: to_uppercase returns a String.
    println!("Upper: {}", s.to_uppercase());
}

The wrapper adds is_near_limit. Everything else comes from String. You can call len, push_str, replace, and any other String method. The compiler handles the routing.

Convention alert: Deref should be cheap and side-effect free. Do not perform allocations, I/O, or logging inside deref. The compiler calls deref implicitly in many places. If deref has side effects, those effects trigger unexpectedly, making the code hard to reason about. If you need side effects, expose a method. Keep deref as a pure pass-through.

Mutable delegation requires DerefMut

The Deref trait only provides access via &self. If your wrapper needs to allow mutation of the inner value, you must implement DerefMut. These are separate traits. DerefMut gives the compiler permission to produce &mut self references through the wrapper.

use std::ops::{Deref, DerefMut};

struct SmartVec<T>(Vec<T>);

impl<T> Deref for SmartVec<T> {
    type Target = Vec<T>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> DerefMut for SmartVec<T> {
    /// Allow mutation of the inner Vec.
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

fn main() {
    let mut v = SmartVec(vec![1, 2, 3]);
    
    // Requires DerefMut. Without it, this fails to compile.
    v.push(4);
    
    println!("{:?}", *v);
}

If you implement Deref but forget DerefMut, the compiler rejects mutation attempts. You get a "no method named push found" error. The compiler tries deref coercion, gets &Vec from Deref, and sees that push requires &mut Vec. The types do not match. The call fails.

Implement both traits when the wrapper supports mutation. The compiler does not auto-implement Deref from DerefMut. You must write both.

Pitfalls and traps

Delegation is convenient, but it introduces subtle traps. Watch for these patterns.

The Clone trap. If your wrapper implements Deref<Target = T> and T implements Clone, calling .clone() on the wrapper returns a T, not the wrapper. The compiler sees the wrapper lacks Clone, coerces to T via Deref, and clones the inner value. The wrapper type vanishes silently.

use std::ops::Deref;

struct Wrapper(String);

impl Deref for Wrapper {
    type Target = String;
    fn deref(&self) -> &String { &self.0 }
}

fn main() {
    let w = Wrapper("hi".to_string());
    
    // Wrapper does not implement Clone.
    // The compiler coerces to String and clones the String.
    // result is String, not Wrapper.
    let result = w.clone();
    
    println!("{:?}", result); // Prints "hi", not Wrapper("hi")
}

Always implement Clone on your wrapper if you want to preserve the type. The compiler will not force you, but your future self will thank you when the type does not vanish.

Method shadowing confusion. If the wrapper and the inner type both have a method with the same name, the wrapper's method wins. This is intentional. It lets you override behavior. It can be confusing if you forget the wrapper has the method. The compiler never falls back to deref if a direct match exists. Check the wrapper first.

Deref for conversion. Do not use Deref to convert between unrelated types. Deref implies a "wrapper" relationship. The wrapper holds the inner value and exposes it. If you want to convert a String to a Vec<char>, use From and Into. Using Deref for arbitrary conversion breaks the mental model and confuses the compiler's coercion logic.

E0277 and trait bounds. If a function requires a trait bound like T: Clone, and you pass a wrapper that delegates to a Clone type but does not implement Clone itself, the compiler may coerce to the inner type to satisfy the bound. This changes the type of the argument. If the function returns the value, you get the inner type back. This is a variant of the Clone trap. Implement the trait on the wrapper to keep the type stable.

Treat Deref as a transparency layer, not a conversion tool. If the wrapper is a distinct concept, stop delegating and start composing.

Decision: delegation versus alternatives

Pick the tool that matches the intent. Transparency gets Deref. Conversion gets From. Distinct identity gets composition.

Use Deref when you are building a smart pointer or wrapper that should transparently expose the inner type's API. The wrapper adds metadata or ownership semantics but should behave like the inner type in all other respects. Examples include Rc, Box, and custom wrappers like BoundedString.

Use DerefMut when the wrapper needs to allow mutation of the inner value through the same transparent interface. Implement DerefMut alongside Deref whenever the wrapper supports mutable access.

Use manual delegation when you need to add logic to specific calls, like logging or validation, without exposing the entire inner API. Forward only the methods you want to support. This keeps the wrapper's surface area small and prevents accidental access to inner methods you do not want to expose.

Use From and Into when you want explicit conversion between types. Conversion should be opt-in. The caller writes .into() or Type::from(). This makes the type change visible in the code. Use this for transforming data, not for wrapping it.

Use composition without Deref when the wrapper is a distinct concept and should not masquerade as the inner type. If the wrapper has its own identity and methods, expose those methods directly. Do not implement Deref. This prevents accidental coercion and makes the API clearer.

Pick the tool that matches the intent. Transparency gets Deref. Conversion gets From. Distinct identity gets composition.

Where to go next