Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
217: The Builder Pattern in Rust
I've seen this happen in almost every mid-sized Rust project I've touched. You start with a simple struct, maybe a HttpRequest or a DatabaseConfig, and you provide a new() method. It works great when you only have two or three fields. But then the requirements creep in. Suddenly, you need a timeout, a custom header map, a retry policy, and a proxy setting. Before you know it, your constructor looks like a telephone number.
The Constructor From Hell
When we're in a rush, the "naive" approach is to just keep adding arguments to the constructor. It looks something like this:
struct HttpRequest {
url: String,
method: String,
timeout: u32,
headers: Vec<String>,
body: Option<String>,
}
impl HttpRequest {
pub fn new(url: String, method: String, timeout: u32, headers: Vec<String>, body: Option<String>) -> Self {
Self { url, method, timeout, headers, body }
}
}
Now, imagine calling this. You're forced to provide every single value, even if you just want the defaults. You end up writing HttpRequest::new("https://api.com".into(), "GET".into(), 30, vec![], None). It's noisy, and it's fragile. If I decide tomorrow that we need a user_agent field, I have to go back and update every single call site in the entire codebase. That's a recipe for a miserable afternoon.
You might be tempted to just make every field an Option and pass a struct of options. But then you've just pushed the problem further down the line; now the logic that actually uses the HttpRequest has to constantly unwrap options or provide defaults, which clutters the business logic with configuration boilerplate.
Separating Intent From Construction
This is where the Builder pattern earns its keep. Instead of one giant function, we create a separate Builder struct. This builder holds the "draft" state of our object. We provide methods to set the optional fields, and only at the very end do we call a build() method to produce the final, immutable HttpRequest.
Here is how I usually structure this in Rust to keep it ergonomic:
#[derive(Debug)]
pub struct HttpRequest {
url: String,
method: String,
timeout: u32,
headers: Vec<String>,
}
pub struct HttpRequestBuilder {
url: String,
method: String,
timeout: u32,
headers: Vec<String>,
}
impl HttpRequestBuilder {
pub fn new(url: String) -> Self {
Self {
url,
method: "GET".to_string(), // Default value
timeout: 30, // Default value
headers: Vec::new(), // Default value
}
}
pub fn method(mut self, method: &str) -> Self {
self.method = method.to_string();
self
}
pub fn timeout(mut self, seconds: u32) -> Self {
self.timeout = seconds;
self
}
pub fn header(mut self, header: String) -> Self {
self.headers.push(header);
self
}
pub fn build(self) -> HttpRequest {
HttpRequest {
url: self.url,
method: self.method,
timeout: self.timeout,
headers: self.headers,
}
}
}
Notice how the methods take mut self and return Self. This allows us to chain calls together. Now, the call site becomes a readable sentence: HttpRequestBuilder::new("https://api.com".into()).method("POST").timeout(60).build(). If we add a new field to the builder, the existing code doesn't break because the new() method handles the default for that new field.
The Cost of the Extra Boilerplate
I'll be honest with you: the Builder pattern is verbose. You're essentially defining your data twice—once in the HttpRequest and once in the HttpRequestBuilder. In a small project, this might feel like overkill. If you only have two optional fields, just use a new() method and a few setter methods on the main struct.
But once you hit that tipping point where your constructor has more than four or five arguments, or where the combination of arguments must be validated (e.g., "if you provide a body, you must also provide a content-length header"), the builder is a lifesaver. It allows you to move the validation logic into the build() method, ensuring that it's impossible to instantiate an "invalid" HttpRequest. You aren't just organizing code; you're using Rust's type system to enforce a correct state.
📋 Practical Task
Implementing a DatabaseConnection Builder
You are building a database library. A DatabaseConnection requires a connection_string (required), but can optionally have a pool_size (default: 10), a timeout (default: 5000ms), and a read_only flag (default: false).
Your task is to implement the Builder pattern for this connection. Create the following:
- A
DatabaseConnectionstruct with the four fields mentioned. - A
DatabaseConnectionBuilderstruct. - A
new(connection_string: String)method for the builder that initializes the defaults. - Fluent methods for
pool_size,timeout, andread_onlythat returnSelf. - A
build(self)method that returns the finalDatabaseConnection.
Test your implementation by creating a connection that uses a custom pool_size and read_only flag, but keeps the default timeout.
There are no comments for now.