Design Patterns in Rust
Implement the builder pattern, newtype pattern, typestate pattern, and RAII in idiomatic Rust.
Builder Pattern
The builder pattern constructs complex objects step by step. In Rust it is especially useful because you cannot have optional fields set to undefined — everything must be initialized.
#[derive(Debug)]
struct Request {
url: String,
method: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout_ms: u64,
follow_redirects: bool,
}
#[derive(Default)]
struct RequestBuilder {
url: Option<String>,
method: String,
headers: Vec<(String, String)>,
body: Option<Vec<u8>>,
timeout_ms: u64,
follow_redirects: bool,
}
impl RequestBuilder {
fn new() -> Self {
Self {
method: "GET".to_string(),
timeout_ms: 30_000,
follow_redirects: true,
..Default::default()
}
}
fn url(mut self, url: &str) -> Self {
self.url = Some(url.to_string());
self
}
fn method(mut self, method: &str) -> Self {
self.method = method.to_uppercase();
self
}
fn header(mut self, key: &str, value: &str) -> Self {
self.headers.push((key.to_string(), value.to_string()));
self
}
fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
self.body = Some(body.into());
self
}
fn timeout(mut self, ms: u64) -> Self {
self.timeout_ms = ms;
self
}
fn build(self) -> Result<Request, String> {
let url = self.url.ok_or("URL is required")?;
Ok(Request {
url,
method: self.method,
headers: self.headers,
body: self.body,
timeout_ms: self.timeout_ms,
follow_redirects: self.follow_redirects,
})
}
}
fn main() {
let request = RequestBuilder::new()
.url("https://api.example.com/users")
.method("POST")
.header("Content-Type", "application/json")
.header("Authorization", "Bearer token123")
.body(r#"{"name":"Alice"}"#)
.timeout(5_000)
.build()
.unwrap();
println!("{:#?}", request);
}
Newtype Pattern
Wrap a primitive in a tuple struct to create a semantically distinct type and prevent mixing values with the same underlying type:
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Meters(f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
struct Seconds(f64);
#[derive(Debug, Clone, Copy)]
struct MetersPerSecond(f64);
impl Meters {
fn value(self) -> f64 { self.0 }
}
impl Seconds {
fn value(self) -> f64 { self.0 }
}
impl std::ops::Div<Seconds> for Meters {
type Output = MetersPerSecond;
fn div(self, t: Seconds) -> MetersPerSecond {
MetersPerSecond(self.0 / t.0)
}
}
// Implement Display
impl std::fmt::Display for Meters {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}m", self.0)
}
}
fn main() {
let distance = Meters(100.0);
let time = Seconds(9.58);
let speed = distance / time;
println!("distance: {}", distance);
println!("speed: {:.2} m/s", speed.0);
// This would be a compile error — you cannot accidentally pass
// Seconds where Meters is expected:
// fn needs_meters(m: Meters) {}
// needs_meters(time); // ERROR: expected Meters, found Seconds
}
Newtype for validated data:
#[derive(Debug, Clone)]
struct EmailAddress(String);
impl EmailAddress {
fn new(s: &str) -> Result<Self, String> {
if s.contains('@') && s.contains('.') {
Ok(EmailAddress(s.to_lowercase()))
} else {
Err(format!("'{}' is not a valid email address", s))
}
}
fn as_str(&self) -> &str { &self.0 }
}
fn send_email(to: &EmailAddress, subject: &str) {
println!("sending '{}' to {}", subject, to.as_str());
}
fn main() {
let email = EmailAddress::new("[email protected]").unwrap();
send_email(&email, "Hello!");
// send_email(&"not-an-email".to_string(), "Hi"); // won't compile
}
Typestate Pattern
Encode state transitions in the type system. Invalid state transitions become compile errors:
use std::marker::PhantomData;
// State types — zero-sized, used only as type markers
struct Locked;
struct Unlocked;
struct Safe<State> {
contents: String,
_state: PhantomData<State>,
}
impl Safe<Locked> {
fn new(contents: &str) -> Self {
Safe { contents: contents.to_string(), _state: PhantomData }
}
fn unlock(self, password: &str) -> Result<Safe<Unlocked>, Safe<Locked>> {
if password == "secret" {
Ok(Safe { contents: self.contents, _state: PhantomData })
} else {
Err(self)
}
}
}
impl Safe<Unlocked> {
fn get_contents(&self) -> &str { &self.contents }
fn put_contents(mut self, contents: &str) -> Self {
self.contents = contents.to_string();
self
}
fn lock(self) -> Safe<Locked> {
Safe { contents: self.contents, _state: PhantomData }
}
}
fn main() {
let safe = Safe::<Locked>::new("diamonds");
// Cannot access contents when locked:
// safe.get_contents(); // ERROR: no method get_contents on Safe<Locked>
let safe = safe.unlock("secret").expect("wrong password");
println!("contents: {}", safe.get_contents());
let safe = safe.put_contents("rubies").lock();
// safe.get_contents(); // ERROR: locked again
println!("safe is locked");
}
RAII — Resource Management
Rust’s Drop trait implements RAII naturally. Resources are always released when the owner goes out of scope:
use std::sync::{Mutex, MutexGuard};
struct ConnectionPool {
connections: Mutex<Vec<String>>,
}
impl ConnectionPool {
fn new(size: usize) -> Self {
let connections = (0..size).map(|i| format!("conn_{}", i)).collect();
Self { connections: Mutex::new(connections) }
}
fn acquire(&self) -> Option<PooledConnection> {
let mut pool = self.connections.lock().unwrap();
pool.pop().map(|conn| PooledConnection {
conn,
pool: &self.connections,
})
}
}
struct PooledConnection<'a> {
conn: String,
pool: &'a Mutex<Vec<String>>,
}
impl<'a> PooledConnection<'a> {
fn execute(&self, query: &str) -> String {
format!("[{}] executed: {}", self.conn, query)
}
}
impl<'a> Drop for PooledConnection<'a> {
fn drop(&mut self) {
// Connection automatically returned to pool
println!("returning {} to pool", self.conn);
self.pool.lock().unwrap().push(self.conn.clone());
}
}
fn main() {
let pool = ConnectionPool::new(3);
{
let conn = pool.acquire().expect("no connections available");
println!("{}", conn.execute("SELECT 1"));
} // conn dropped here — automatically returned to pool
println!("pool size: {}", pool.connections.lock().unwrap().len());
}
Strategy Pattern with Closures
struct Sorter<T, F: Fn(&T, &T) -> std::cmp::Ordering> {
comparator: F,
_phantom: std::marker::PhantomData<T>,
}
impl<T, F: Fn(&T, &T) -> std::cmp::Ordering> Sorter<T, F> {
fn new(comparator: F) -> Self {
Self { comparator, _phantom: std::marker::PhantomData }
}
fn sort(&self, data: &mut Vec<T>) {
data.sort_by(|a, b| (self.comparator)(a, b));
}
}
fn main() {
let mut numbers = vec![3, 1, 4, 1, 5, 9, 2, 6];
let asc_sorter = Sorter::new(|a: &i32, b| a.cmp(b));
asc_sorter.sort(&mut numbers);
println!("{:?}", numbers);
let desc_sorter = Sorter::new(|a: &i32, b| b.cmp(a));
desc_sorter.sort(&mut numbers);
println!("{:?}", numbers);
// Sort strings by length
let mut words = vec!["banana", "apple", "cherry", "fig"];
let len_sorter = Sorter::new(|a: &&str, b: &&str| a.len().cmp(&b.len()));
len_sorter.sort(&mut words);
println!("{:?}", words); // ["fig", "apple", "banana", "cherry"]
}
Observer Pattern with Trait Objects
trait Observer {
fn on_event(&self, event: &str);
}
struct EventSource {
observers: Vec<Box<dyn Observer>>,
}
impl EventSource {
fn new() -> Self { Self { observers: Vec::new() } }
fn subscribe(&mut self, obs: Box<dyn Observer>) {
self.observers.push(obs);
}
fn emit(&self, event: &str) {
for obs in &self.observers {
obs.on_event(event);
}
}
}
struct Logger { prefix: String }
struct Counter { count: std::cell::Cell<u32> }
impl Observer for Logger {
fn on_event(&self, event: &str) {
println!("[{}] {}", self.prefix, event);
}
}
impl Observer for Counter {
fn on_event(&self, _event: &str) {
self.count.set(self.count.get() + 1);
}
}
fn main() {
let mut source = EventSource::new();
source.subscribe(Box::new(Logger { prefix: "LOG".to_string() }));
source.subscribe(Box::new(Counter { count: std::cell::Cell::new(0) }));
source.emit("user_login");
source.emit("page_view");
source.emit("user_logout");
} Frequently Asked Questions
Does Rust have traditional OOP design patterns?
Many OOP patterns (strategy, observer, factory) translate directly using traits and closures. Some patterns like inheritance don't exist, but Rust's ownership model enables unique patterns like typestate and RAII.
What is the typestate pattern?
The typestate pattern encodes state transitions in the type system. Invalid transitions become compile errors rather than runtime errors.
What is the newtype pattern?
The newtype pattern wraps an existing type in a single-field tuple struct to create a distinct type. It prevents mixing up logically different values of the same underlying type.