Rust Design Patterns
Rust's type system enables powerful design patterns that are either impossible or impractical in other languages. These patterns leverage ownership, traits, and enums to encode correctness into the type system itself.
Builder Pattern
#[derive(Debug)]
struct Server {
host: String,
port: u16,
max_connections: u32,
tls: bool,
timeout_secs: u64,
}
struct ServerBuilder {
host: String,
port: u16,
max_connections: u32,
tls: bool,
timeout_secs: u64,
}
impl ServerBuilder {
fn new(host: impl Into) -> Self {
ServerBuilder {
host: host.into(),
port: 8080, // Default values
max_connections: 100,
tls: false,
timeout_secs: 30,
}
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn max_connections(mut self, max: u32) -> Self {
self.max_connections = max;
self
}
fn tls(mut self, enabled: bool) -> Self {
self.tls = enabled;
self
}
fn timeout(mut self, secs: u64) -> Self {
self.timeout_secs = secs;
self
}
fn build(self) -> Server {
Server {
host: self.host,
port: self.port,
max_connections: self.max_connections,
tls: self.tls,
timeout_secs: self.timeout_secs,
}
}
}
fn main() {
let server = ServerBuilder::new("localhost")
.port(443)
.tls(true)
.max_connections(1000)
.timeout(60)
.build();
println!("{:?}", server);
}
Newtype Pattern
// Wrap primitive types to add type safety
#[derive(Debug, Clone, Copy, PartialEq)]
struct Meters(f64);
#[derive(Debug, Clone, Copy, PartialEq)]
struct Kilometers(f64);
#[derive(Debug, Clone, Copy, PartialEq)]
struct Miles(f64);
impl Meters {
fn to_kilometers(self) -> Kilometers {
Kilometers(self.0 / 1000.0)
}
}
impl Kilometers {
fn to_miles(self) -> Miles {
Miles(self.0 * 0.621371)
}
}
// Now you can't accidentally mix units!
fn calculate_fuel(distance: Kilometers, efficiency: f64) -> f64 {
distance.0 * efficiency
}
fn main() {
let distance = Meters(5000.0);
let km = distance.to_kilometers();
let fuel = calculate_fuel(km, 0.08);
println!("{:?} = {:?}, fuel: {fuel:.1}L", distance, km);
// This won't compile — type safety!
// calculate_fuel(distance, 0.08); // Expected Kilometers, got Meters
}
// Also great for validated types
struct Email(String);
impl Email {
fn new(s: &str) -> Result {
if s.contains('@') {
Ok(Email(s.to_string()))
} else {
Err("Invalid email".to_string())
}
}
fn as_str(&self) -> &str {
&self.0
}
}
Typestate Pattern
// Encode state machine transitions in the type system
// Invalid transitions become COMPILE ERRORS
struct Draft;
struct Review;
struct Published;
struct BlogPost {
title: String,
content: String,
_state: std::marker::PhantomData,
}
impl BlogPost {
fn new(title: String) -> Self {
BlogPost {
title,
content: String::new(),
_state: std::marker::PhantomData,
}
}
fn set_content(mut self, content: String) -> Self {
self.content = content;
self
}
fn submit_for_review(self) -> BlogPost {
BlogPost {
title: self.title,
content: self.content,
_state: std::marker::PhantomData,
}
}
}
impl BlogPost {
fn approve(self) -> BlogPost {
BlogPost {
title: self.title,
content: self.content,
_state: std::marker::PhantomData,
}
}
fn reject(self) -> BlogPost {
BlogPost {
title: self.title,
content: self.content,
_state: std::marker::PhantomData,
}
}
}
impl BlogPost {
fn url(&self) -> String {
format!("/blog/{}", self.title.to_lowercase().replace(' ', "-"))
}
}
fn main() {
let post = BlogPost::::new("Rust Patterns".into())
.set_content("Great patterns!".into())
.submit_for_review()
.approve();
println!("Published at: {}", post.url());
// This won't compile — can't publish a draft!
// let draft = BlogPost::::new("Test".into());
// draft.url(); // ERROR: no method url on BlogPost
}
State Machine with Enums
#[derive(Debug)]
enum ConnectionState {
Disconnected,
Connecting { attempt: u32 },
Connected { session_id: String },
Error { message: String, retries: u32 },
}
impl ConnectionState {
fn connect(self) -> Self {
match self {
ConnectionState::Disconnected => {
ConnectionState::Connecting { attempt: 1 }
}
ConnectionState::Error { retries, .. } if retries < 3 => {
ConnectionState::Connecting { attempt: retries + 1 }
}
other => other, // Already connecting/connected
}
}
fn on_success(self, session_id: String) -> Self {
match self {
ConnectionState::Connecting { .. } => {
ConnectionState::Connected { session_id }
}
other => other,
}
}
fn on_error(self, message: String) -> Self {
match self {
ConnectionState::Connecting { attempt } => {
ConnectionState::Error { message, retries: attempt }
}
other => other,
}
}
fn disconnect(self) -> Self {
ConnectionState::Disconnected
}
}
fn main() {
let mut state = ConnectionState::Disconnected;
println!("{:?}", state);
state = state.connect();
println!("{:?}", state);
state = state.on_success("abc-123".into());
println!("{:?}", state);
state = state.disconnect();
println!("{:?}", state);
}
RAII Pattern
// Resource Acquisition Is Initialization
// Rust's Drop trait ensures cleanup happens automatically
struct TempFile {
path: String,
}
impl TempFile {
fn new(path: &str) -> std::io::Result {
std::fs::write(path, "")?;
println!("Created temp file: {path}");
Ok(TempFile { path: path.to_string() })
}
fn write(&self, data: &str) -> std::io::Result<()> {
std::fs::write(&self.path, data)
}
}
impl Drop for TempFile {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
println!("Cleaned up temp file: {}", self.path);
}
}
fn main() -> std::io::Result<()> {
{
let temp = TempFile::new("/tmp/rust_temp.txt")?;
temp.write("temporary data")?;
// temp is automatically cleaned up here
} // Drop is called — file is deleted
println!("Temp file has been cleaned up!");
Ok(())
}
Key Takeaways
- ✅ The Builder pattern provides ergonomic construction of complex types with defaults
- ✅ Newtype pattern adds type safety by wrapping primitives in distinct types
- ✅ Typestate pattern encodes valid state transitions in the type system
- ✅ Enums model state machines where each variant holds state-specific data
- ✅ RAII with Drop ensures resources are automatically cleaned up