Rust Builder Pattern
The builder pattern builds a value step by step, through a helper struct. A final build() call produces the finished value.
The long way: one long argument list
Without a builder, one call must pass every field in order, and the bare values do not say what they mean:
let config = ServerConfig::new(String::from("example.com"), 3000, true);
println!("{}:{} debug={}", config.host, config.port, config.debug);The builder gathers settings
The builder is a helper struct with the same fields as the real one. Its new() fills every field with a sensible default.
fn new() -> ServerConfigBuilder {
ServerConfigBuilder { host: String::from("localhost"), port: 8080, debug: false }
}One setter per field
Each setter changes one field and returns &mut Self — that return is what makes chaining work.
fn port(&mut self, port: u16) -> &mut Self {
self.port = port;
self
}Setters chain into build()
Set only the fields you care about, in any order, then call build().
let custom = ServerConfigBuilder::new()
.host("example.com")
.port(3000)
.debug(true)
.build();
println!("{:?}", custom);No setters? You get the defaults
Call build() straight away and every default survives.
let plain = ServerConfigBuilder::new().build();
println!("{:?}", plain);A field with no safe default
Store it as an Option in the builder. build() can then return a Result, with an Err when the field was never set.
struct ApiBuilder {
key: Option<String>, // None until the caller sets it
}
// build() checks the Option and returns Err if it is still NoneExample
#[derive(Debug)]
struct ServerConfig {
host: String,
port: u16,
debug: bool,
}
struct ServerConfigBuilder {
host: String,
port: u16,
debug: bool,
}
impl ServerConfigBuilder {
fn new() -> ServerConfigBuilder {
ServerConfigBuilder { host: String::from("localhost"), port: 8080, debug: false }
}
fn host(&mut self, host: &str) -> &mut Self {
self.host = host.to_string();
self
}
fn port(&mut self, port: u16) -> &mut Self {
self.port = port;
self
}
fn debug(&mut self, debug: bool) -> &mut Self {
self.debug = debug;
self
}
fn build(&self) -> ServerConfig {
ServerConfig { host: self.host.clone(), port: self.port, debug: self.debug }
}
}
fn main() {
let plain = ServerConfigBuilder::new().build();
println!("{:?}", plain);
let custom = ServerConfigBuilder::new().host("example.com").port(3000).debug(true).build();
println!("{:?}", custom);
}Key points
- This pattern came earlier: Tokio's runtime Builder, from the Tokio Runtime lesson, works exactly this way
In this example, ServerConfig is the finished value: a host, a port and a debug flag. ServerConfigBuilder is the helper that gathers the settings. Its new() fills in the defaults — localhost, 8080, false. Each setter changes one field and returns &mut Self, so the setters chain. The plain config calls build() straight away and keeps every default. The custom config first overrides the host, the port and the debug flag, then builds. Notice that build() clones the host String out of the builder. The same builder could build again with different settings.
Note
In many languages, builders exist because constructors with ten parameters are painful.
Rust has no named or optional parameters, so the pattern earns its keep here too.
The call site reads like a list of choices, not a row of mystery values.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why does each setter return &mut Self?