The Chain of Responsibility (CoR) pattern is a behavioral design pattern where a request is passed along a chain of handlers — each handler either processes the request or forwards it to the next one in the chain.

The three key components are: ​

  • Handler — a trait defining the interface for handling requests and holding the successor link

  • Successor — the next handler in the chain, stored as Option<Box>

  • Request — the data passed along the chain

The Rust community’s guiding principle here is: static where you can, dynamic where you must.

Example 1: Purchase Approval Chain (Dynamic Disptach)

The key pattern here is the Option<Box> field — it lets each handler optionally own its successor and forward the request via next.process_request(request).

struct PurchaseRequest {
    amount: f64,
}
 
trait Approver {
    fn set_successor(&mut self, successor: Box<dyn Approver>);
    fn process_request(&self, request: &PurchaseRequest);
}
 
struct Manager {
    successor: Option<Box<dyn Approver>>,
}
 
impl Approver for Manager {
    fn set_successor(&mut self, successor: Box<dyn Approver>) {
        self.successor = Some(successor);
    }
    fn process_request(&self, request: &PurchaseRequest) {
        if request.amount <= 1000.0 {
            println!("Manager approves ${}", request.amount);
        } else if let Some(ref next) = self.successor {
            next.process_request(request); // pass it up
        } else {
            println!("Cannot be approved.");
        }
    }
}
 
struct Director { successor: Option<Box<dyn Approver>> }
 
impl Approver for Director {
    fn set_successor(&mut self, successor: Box<dyn Approver>) {
        self.successor = Some(successor);
    }
    fn process_request(&self, request: &PurchaseRequest) {
        if request.amount <= 5000.0 {
            println!("Director approves ${}", request.amount);
        } else if let Some(ref next) = self.successor {
            next.process_request(request);
        } else {
            println!("Cannot be approved.");
        }
    }
}
 
struct President;
 
impl Approver for President {
    fn set_successor(&mut self, _: Box<dyn Approver>) {} // terminal node
    fn process_request(&self, request: &PurchaseRequest) {
        if request.amount <= 10000.0 {
            println!("President approves ${}", request.amount);
        } else {
            println!("Request denied.");
        }
    }
}
 
fn main() {
    let president = President;
    let mut director = Director { successor: Some(Box::new(president)) };
    let mut manager = Manager { successor: Some(Box::new(director)) };
 
    manager.process_request(&PurchaseRequest { amount: 500.0 });   // Manager approves
    manager.process_request(&PurchaseRequest { amount: 3000.0 });  // Director approves
    manager.process_request(&PurchaseRequest { amount: 8000.0 });  // President approves
    manager.process_request(&PurchaseRequest { amount: 15000.0 }); // Denied
}

Example 2: Purchase Approval Chain (Generics - Zero-cost static dispatch)

This is verbose but gives you zero-cost static dispatch — no heap allocation, no vtable lookup, all method calls resolved at compile time.

Handler<Manager<Director<President>>>

trait Handler {
    fn handle(&self, amount: f64);
}
 
// Terminal handler — no successor
struct President;
 
impl Handler for President {
    fn handle(&self, amount: f64) {
        if amount <= 10_000.0 {
            println!("President approves ${amount}");
        } else {
            println!("Request denied.");
        }
    }
}
 
// Generic handler wrapping the next handler N
struct Manager<N: Handler> {
    next: N,
}
 
impl<N: Handler> Handler for Manager<N> {
    fn handle(&self, amount: f64) {
        if amount <= 1_000.0 {
            println!("Manager approves ${amount}");
        } else {
            self.next.handle(amount); // static dispatch!
        }
    }
}
 
struct Director<N: Handler> {
    next: N,
}
 
impl<N: Handler> Handler for Director<N> {
    fn handle(&self, amount: f64) {
        if amount <= 5_000.0 {
            println!("Director approves ${amount}");
        } else {
            self.next.handle(amount);
        }
    }
}
 
fn main() {
    // The full type is Manager<Director<President>>
    let chain = Manager {
        next: Director {
            next: President,
        },
    };
 
    chain.handle(500.0);    // Manager approves
    chain.handle(3_000.0);  // Director approves
    chain.handle(8_000.0);  // President approves
    chain.handle(15_000.0); // Request denied
}

The compiler monomorphizes this — it generates a unique, optimized function for each concrete type combination. No heap, no vtable.

Using impl Trait to Hide the Chain Type

If you want to hide the ugly Manager<Director> type from callers, use impl Trait as a return type:

fn build_chain() -> impl Handler {
    Manager {
        next: Director {
            next: President,
        },
    }
}
 
fn main() {
    let chain = build_chain(); // type is opaque to the caller
    chain.handle(3_000.0);
}

This still uses static dispatch under the hood — the compiler knows the exact type, the caller just doesn’t need to spell it out.

Example 3: Enum-Based Variant (More Idiomatic)

Rust’s enums offer a cleaner alternative when the set of handlers is fixed at compile time:

enum SupportLevel {
    Basic,
    Intermediate,
    Critical,
}
 
fn handle_request(level: SupportLevel) {
    match level {
        SupportLevel::Basic       => println!("L1 Support handled it"),
        SupportLevel::Intermediate => println!("L2 Support handled it"),
        SupportLevel::Critical    => println!("L3 Support handled it"),
    }
}

This approach avoids heap allocation and dynamic dispatch entirely, but loses the runtime flexibility of adding or reordering handlers.