10 Rust Design Patterns Every Developer Should Master in 2025
Rust forces you to think differently. The patterns that work in Java or Python often don’t translate. Here are the patterns that actually matter when writing Rust code.
1. Newtype Pattern
Wrap primitives to add type safety. Prevents mixing up values that happen to have the same type.
struct UserId(i32);
struct ProductId(i32);
fn get_user(id: UserId) -> User {
// can't accidentally pass ProductId here
}
let user_id = UserId(42);
let product_id = ProductId(42);
You can’t accidentally pass a ProductId where a UserId is expected. The compiler stops you.
2. Builder Pattern
Complex structs need many parameters. Builders make this clean.
struct Server {
host: String,
port: u16,
timeout: u64,
max_connections: usize,
}
impl Server {
fn builder() -> ServerBuilder {
ServerBuilder::default()
}
}
struct ServerBuilder {
host: String,
port: u16,
timeout: u64,
max_connections: usize,
}
impl ServerBuilder {
fn host(mut self, host: String) -> Self {
self.host = host;
self
}
fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
fn build(self) -> Server {
Server {
host: self.host,
port: self.port,
timeout: self.timeout,
max_connections: self.max_connections,
}
}
}
impl Default for ServerBuilder {
fn default() -> Self {
ServerBuilder {
host: String::from("localhost"),
port: 8080,
timeout: 30,
max_connections: 100,
}
}
}
let server = Server::builder()
.host(String::from("0.0.0.0"))
.port(3000)
.build();
3. The Option and Result Pattern
Rust has no null. You handle absence explicitly.
fn find_user(id: i32) -> Option<User> {
if id > 0 {
Some(User { id, name: String::from("Alice") })
} else {
None
}
}
match find_user(5) {
Some(user) => println!("Found: {}", user.name),
None => println!("User not found"),
}
For errors, use Result:
fn parse_age(input: &str) -> Result<u32, String> {
input.parse::<u32>()
.map_err(|_| String::from("Invalid age"))
}
match parse_age("25") {
Ok(age) => println!("Age: {}", age),
Err(e) => println!("Error: {}", e),
}
4. Strategy Pattern with Traits
Different algorithms, same interface.
trait PaymentProcessor {
fn process(&self, amount: f64) -> Result<String, String>;
}
struct CreditCard;
struct PayPal;
impl PaymentProcessor for CreditCard {
fn process(&self, amount: f64) -> Result<String, String> {
Ok(format!("Charged ${} to card", amount))
}
}
impl PaymentProcessor for PayPal {
fn process(&self, amount: f64) -> Result<String, String> {
Ok(format!("Paid ${} via PayPal", amount))
}
}
fn checkout(processor: &dyn PaymentProcessor, amount: f64) {
match processor.process(amount) {
Ok(msg) => println!("{}", msg),
Err(e) => println!("Payment failed: {}", e),
}
}
checkout(&CreditCard, 99.99);
checkout(&PayPal, 49.99);
5. RAII Pattern
Resources clean themselves up automatically. No manual cleanup needed.
use std::fs::File;
use std::io::Write;
fn write_log(message: &str) {
let mut file = File::create("log.txt").unwrap();
file.write_all(message.as_bytes()).unwrap();
}
When file goes out of scope, it closes automatically. This works for database connections, network sockets, everything.
6. State Pattern with Enums
Model different states as enum variants.
enum ConnectionState {
Disconnected,
Connecting,
Connected { session_id: String },
Failed { error: String },
}
struct Connection {
state: ConnectionState,
}
impl Connection {
fn new() -> Self {
Connection {
state: ConnectionState::Disconnected,
}
}
fn connect(&mut self, session: String) {
self.state = ConnectionState::Connected { session_id: session };
}
fn status(&self) -> &str {
match &self.state {
ConnectionState::Disconnected => "Not connected",
ConnectionState::Connecting => "Connecting...",
ConnectionState::Connected { session_id } => "Connected",
ConnectionState::Failed { error } => "Failed",
}
}
}
7. Iterator Pattern
Everything iterable in Rust follows the same pattern.
struct Counter {
count: u32,
max: u32,
}
impl Counter {
fn new(max: u32) -> Self {
Counter { count: 0, max }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < self.max {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
let counter = Counter::new(3);
for num in counter {
println!("{}", num);
}
8. Extension Traits
Add methods to types you don’t own.
rust
trait StringHelpers {
fn truncate(&self, len: usize) -> String;
}
impl StringHelpers for String {
fn truncate(&self, len: usize) -> String {
if self.len() > len {
format!("{}...", &self[..len])
} else {
self.clone()
}
}
}
let long_text = String::from("This is a very long string");
println!("{}", long_text.truncate(10));
9. The Visitor Pattern with Trait Objects
Process different types uniformly.
trait Visitor {
fn visit_number(&self, n: i32);
fn visit_text(&self, s: &str);
}
struct PrintVisitor;
impl Visitor for PrintVisitor {
fn visit_number(&self, n: i32) {
println!("Number: {}", n);
}
fn visit_text(&self, s: &str) {
println!("Text: {}", s);
}
}
trait Visitable {
fn accept(&self, visitor: &dyn Visitor);
}
enum Data {
Number(i32),
Text(String),
}
impl Visitable for Data {
fn accept(&self, visitor: &dyn Visitor) {
match self {
Data::Number(n) => visitor.visit_number(*n),
Data::Text(s) => visitor.visit_text(s),
}
}
}
10. Smart Pointer Patterns
Use Box, Rc, and Arc when ownership gets complex.
use std::rc::Rc;
struct Node {
value: i32,
next: Option<Box<Node>>,
}
fn create_list() -> Node {
Node {
value: 1,
next: Some(Box::new(Node {
value: 2,
next: None,
})),
}
}
For shared ownership:
use std::rc::Rc;
struct SharedData {
value: String,
}
let data = Rc::new(SharedData {
value: String::from("shared"),
});
let reference1 = Rc::clone(&data);
let reference2 = Rc::clone(&data);
Pattern Flow
Input Data
↓
┌─────────────────┐
│ Newtype Wrap │ ← Type Safety
└─────────────────┘
↓
┌─────────────────┐
│ Builder Pattern │ ← Construction
└─────────────────┘
↓
┌─────────────────┐
│ Option/Result │ ← Error Handling
└─────────────────┘
↓
┌─────────────────┐
│ Trait Strategy │ ← Polymorphism
└─────────────────┘
↓
┌─────────────────┐
│ RAII Cleanup │ ← Resource Management
└─────────────────┘
Why These Matter
These aren’t academic exercises. They’re how production Rust code actually works. The Newtype pattern prevents bugs that unit tests might miss. The Builder pattern makes APIs usable. RAII prevents resource leaks. Result and Option force you to handle errors.
Other patterns exist. But if you master these ten, you’ll write Rust that feels natural instead of fighting the compiler every step.
Start with Option and Result. Get comfortable with pattern matching. Then move to traits and builders. The rest will click once you understand Rust’s ownership model.
These patterns aren’t unique to Rust, but Rust’s type system makes them more powerful and safer than in other languages. That’s the whole point.