Struct Design Patterns
Idiomatic Rust code uses four core struct design patterns—the Newtype pattern, the Builder pattern, the Typestate pattern, and the RAII Guard pattern—to turn domain rules and resource lifecycles into compile-time guarantees with zero runtime overhead.
Overview of Core Struct Patterns
| Pattern | Structure | Problem Solved |
|---|---|---|
| Newtype | Single-field tuple struct struct Miles(f64); |
Prevents mixing up primitive values of the same underlying type (UserId(u64) vs. OrderId(u64)) and circumvents the orphan rule |
| Builder | Helper struct ServerBuilder with chained methods ending in .build() |
Replaces multi-argument constructors or telescoping new_* functions when many optional fields exist |
| Typestate | Generic phantom marker File<Open> vs. File<Closed> |
Encodes valid state transitions into the type system so invalid method calls fail at compile time |
| RAII Guard | Struct implementing Drop (e.g., MutexGuard, TempDir) |
Guarantees deterministic cleanup (unlocking, rolling back, closing) on every return path and panic |
Pattern 1: The Newtype Pattern for Type-Safe Domain Primitives
Wrapping a primitive type in a single-field tuple struct (#[repr(transparent)]) has the exact same memory representation and performance as the inner type, while making accidental unit or ID mix-ups a compile error:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct UserId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct InvoiceId(u64);
fn fetch_invoice(user: UserId, invoice: InvoiceId) {
println!("Fetching invoice #{} for user #{}", invoice.0, user.0);
}
fn main() {
let uid = UserId(1001);
let iid = InvoiceId(5500);
fetch_invoice(uid, iid);
// `fetch_invoice(iid, uid)` would fail at compile time with mismatched types!
}
Fetching invoice #5500 for user #1001
Pattern 2: The Builder Pattern for Complex Configuration
When a struct has required parameters alongside several optional settings, a dedicated builder struct collects configuration step-by-step and validates or constructs the target struct in .build():
#[derive(Debug)]
pub struct ConnectionPool {
url: String,
max_connections: usize,
idle_timeout_secs: u64,
}
pub struct ConnectionPoolBuilder {
url: String,
max_connections: usize,
idle_timeout_secs: u64,
}
impl ConnectionPoolBuilder {
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
max_connections: 10,
idle_timeout_secs: 60,
}
}
pub fn max_connections(mut self, max: usize) -> Self {
self.max_connections = max;
self
}
pub fn idle_timeout_secs(mut self, secs: u64) -> Self {
self.idle_timeout_secs = secs;
self
}
pub fn build(self) -> Result<ConnectionPool, &'static str> {
if self.max_connections == 0 {
return Err("max_connections must be at least 1");
}
Ok(ConnectionPool {
url: self.url,
max_connections: self.max_connections,
idle_timeout_secs: self.idle_timeout_secs,
})
}
}
fn main() {
let pool = ConnectionPoolBuilder::new("postgres://localhost/app")
.max_connections(32)
.idle_timeout_secs(120)
.build()
.unwrap();
println!("{pool:?} (url = {})", pool.url);
}
ConnectionPool { url: "postgres://localhost/app", max_connections: 32, idle_timeout_secs: 120 } (url = postgres://localhost/app)
Pattern 3: The RAII Scope Guard Pattern
By tying resource acquisition to struct initialization and resource release to Drop::drop(&mut self), a guard struct ensures cleanup always runs when the scope exits:
struct TimerGuard {
operation: &'static str,
}
impl TimerGuard {
fn start(operation: &'static str) -> Self {
println!("[START] {}", operation);
Self { operation }
}
}
impl Drop for TimerGuard {
fn drop(&mut self) {
println!("[FINISH] {}", self.operation);
}
}
fn main() {
{
let _guard = TimerGuard::start("Database Migration");
println!(" Running migration steps...");
} // `_guard` is automatically dropped here!
}
[START] Database Migration
Running migration steps...
[FINISH] Database Migration