🎓 A1Learner
🔍

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 None

Example

#[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?