The Rust builder pattern is a way to construct complex objects step-by-step, ensuring that the object is always in a valid state and that you don’t end up with half-built, unusable messes.

Let’s see it in action. Imagine we’re building a User struct.

struct User {
    username: String,
    email: String,
    age: Option<u32>,
    is_active: bool,
}

impl User {
    fn builder() -> UserBuilder {
        UserBuilder {
            username: String::new(),
            email: String::new(),
            age: None,
            is_active: false,
        }
    }
}

struct UserBuilder {
    username: String,
    email: String,
    age: Option<u32>,
    is_active: bool,
}

impl UserBuilder {
    fn username(mut self, username: String) -> Self {
        self.username = username;
        self
    }

    fn email(mut self, email: String) -> Self {
        self.email = email;
        self
    }

    fn age(mut self, age: u32) -> Self {
        self.age = Some(age);
        self
    }

    fn is_active(mut self, is_active: bool) -> Self {
        self.is_active = is_active;
        self
    }

    fn build(self) -> Result<User, String> {
        if self.username.is_empty() {
            return Err("Username cannot be empty".to_string());
        }
        if self.email.is_empty() {
            return Err("Email cannot be empty".to_string());
        }
        Ok(User {
            username: self.username,
            email: self.email,
            age: self.age,
            is_active: self.is_active,
        })
    }
}

fn main() {
    let user = User::builder()
        .username("alice".to_string())
        .email("alice@example.com".to_string())
        .age(30)
        .is_active(true)
        .build();

    match user {
        Ok(u) => println!("Created user: {} ({})", u.username, u.email),
        Err(e) => println!("Error creating user: {}", e),
    }

    let invalid_user = User::builder()
        .email("bob@example.com".to_string())
        .build(); // Missing username

    match invalid_user {
        Ok(u) => println!("Created user: {} ({})", u.username, u.email),
        Err(e) => println!("Error creating user: {}", e),
    }
}

This UserBuilder struct has methods for each field of the User. Each method takes self by value, modifies the builder, and returns self. This chaining is what makes it a builder. The build method performs final validation before returning a Result<User, String>.

The problem this solves is the telescoping constructor anti-pattern. If User had many optional fields, you’d end up with a constructor like User::new(username, email, age, address, phone, is_active, ...) where many arguments would be None or default values, making it hard to read and prone to errors. The builder pattern makes it explicit which fields you’re setting and in what order.

Internally, the builder pattern works by having a separate struct (UserBuilder) that holds the intermediate state of the object being built. Each "setter" method on the builder takes ownership of the builder (mut self), modifies its internal state, and then returns ownership of the builder. This allows for chaining. The build method consumes the builder (self) and produces the final object, often performing validation at this stage.

You control the construction by calling the builder methods in any order you choose, and by deciding which optional fields to set. The build method is the gatekeeper, ensuring that the object is valid before it’s created.

A common pitfall is forgetting to handle the Result returned by build(). If your builder has validation logic, the build method will return an Err if the object isn’t valid, and you need to gracefully handle that.

The true power of this pattern in Rust comes from its interaction with the type system. By returning Self from each builder method, you ensure that the builder itself is always of the correct type, and by returning Result<User, String> from build, you enforce that the final User object is only produced if it meets all the defined criteria.

When you implement From<UserBuilder> for User, you can simplify the build method to just self.into(), letting Rust handle the conversion.

Want structured learning?

Take the full Rust course →