Das Builder-Pattern ist die Standard-Antwort auf das Fehlen von Default-Parametern und überladenen Konstruktoren in Rust. Wenn ein Typ viele optionale Konfigurations-Werte hat, ist ein einziger new(...)-Konstruktor unhandlich — entweder zu viele Parameter, oder eine Lawine von new_with_a, new_with_a_and_b, new_with_everything. Der Builder löst das durch schrittweisen Aufbau mit Method-Chaining: erst Builder::neu(), dann konfigurieren mit .timeout(...).retries(...).header(...), am Ende .bauen(). Dieser Artikel zeigt die zwei Builder-Stile (konsumierend mit self vs. mutable mit &mut self), Fehlerbehandlung und das derive_builder-Crate.

Das Problem

Stell dir vor, du baust eine HTTP-Anfrage. Sie hat URL (Pflicht), Method (Default GET), Timeout (Default 5s), Headers (optional), Body (optional). Naive Lösung:

Rust Vieler Konstruktor — schlecht
struct Anfrage { /* ... */ }

impl Anfrage {
    // Variante 1: alle Parameter
    fn new(url: String, method: String, timeout_ms: u32,
           headers: Vec<(String, String)>, body: Vec<u8>) -> Self {
        # Anfrage {}
    }
    // Variante 2: nur URL — Defaults für Rest
    fn new_simple(url: String) -> Self { todo!() }
    // Variante 3: URL + Method
    fn new_with_method(url: String, method: String) -> Self { todo!() }
    // ... etc.
}

Schnell unübersichtlich. Lösung: Builder.

Konsumierender Builder (self-Style)

Der klassische Stil: jede Builder-Methode nimmt mut self, konfiguriert intern und gibt Self zurück.

Rust Konsumierender Builder
pub struct AnfrageBuilder {
    url: String,
    method: String,
    timeout_ms: u32,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
}

impl AnfrageBuilder {
    pub fn neu(url: impl Into<String>) -> Self {
        AnfrageBuilder {
            url: url.into(),
            method: "GET".into(),
            timeout_ms: 5000,
            headers: Vec::new(),
            body: Vec::new(),
        }
    }

    pub fn method(mut self, m: &str) -> Self {
        self.method = m.to_string();
        self
    }

    pub fn timeout(mut self, ms: u32) -> Self {
        self.timeout_ms = ms;
        self
    }

    pub fn header(mut self, key: &str, wert: &str) -> Self {
        self.headers.push((key.to_string(), wert.to_string()));
        self
    }

    pub fn body(mut self, b: Vec<u8>) -> Self {
        self.body = b;
        self
    }

    pub fn bauen(self) -> Anfrage {
        Anfrage {
            url: self.url,
            method: self.method,
            timeout_ms: self.timeout_ms,
            headers: self.headers,
            body: self.body,
        }
    }
}

pub struct Anfrage {
    pub url: String,
    pub method: String,
    pub timeout_ms: u32,
    pub headers: Vec<(String, String)>,
    pub body: Vec<u8>,
}

fn main() {
    let anfrage = AnfrageBuilder::neu("https://example.com/api")
        .method("POST")
        .timeout(10_000)
        .header("Accept", "application/json")
        .header("X-Token", "abc123")
        .body(b"{}".to_vec())
        .bauen();
    println!("{}: {}", anfrage.method, anfrage.url);
}

Eigenschaften:

  • mut self als Receiver — der Builder wird durch jede Methode gemoved.
  • Self-Return — die Methode gibt sich selbst (modifiziert) zurück.
  • bauen(self) konsumiert — wandelt den Builder in den finalen Typ um.
  • Aufrufer braucht keine mut-Bindung — alles in einer Chain.

Mutable Builder (&mut-Style)

Alternative: jede Methode nimmt &mut self und gibt &mut Self zurück.

Rust Mutable Builder
pub struct UrlBuilder {
    schema: String,
    host: String,
    pfad: String,
    params: Vec<(String, String)>,
}

impl UrlBuilder {
    pub fn neu(host: impl Into<String>) -> Self {
        UrlBuilder {
            schema: "https".into(),
            host: host.into(),
            pfad: "/".into(),
            params: Vec::new(),
        }
    }

    pub fn pfad(&mut self, p: &str) -> &mut Self {
        self.pfad = p.to_string();
        self
    }

    pub fn param(&mut self, key: &str, wert: &str) -> &mut Self {
        self.params.push((key.into(), wert.into()));
        self
    }

    pub fn bauen(&self) -> String {
        let query = if self.params.is_empty() {
            String::new()
        } else {
            let p: Vec<String> = self.params.iter()
                .map(|(k, v)| format!("{k}={v}"))
                .collect();
            format!("?{}", p.join("&"))
        };
        format!("{}://{}{}{}", self.schema, self.host, self.pfad, query)
    }
}

fn main() {
    let mut b = UrlBuilder::neu("api.example.com");
    b.pfad("/users").param("page", "1").param("limit", "20");
    let url = b.bauen();
    // Builder ist hier noch nutzbar — kann weiter konfiguriert werden
    assert!(url.contains("page=1"));
}

Unterschiede:

  • &mut self als Receiver — der Builder bleibt beim Aufrufer.
  • &mut Self-Return für Chaining.
  • Aufrufer braucht mut-Bindunglet mut b = ....
  • bauen(&self) mit Borrow — der Builder existiert nach dem Bauen weiter.

Wann welcher Stil?

StilWann nutzen
Konsumierend (self + Self)Klassisch — Builder wird genau einmal verwendet
Mutable (&mut self + &mut Self)Wenn der Builder mehrfach genutzt oder zwischen Funktionen geteilt wird

Konsumierend ist häufiger und prägnanter. Mutable ist nützlich, wenn der Builder „bestehen bleiben" soll oder seine Methoden aus verschiedenen Stellen aufgerufen werden.

Fallible Builder mit Result-Rückgabe

Wenn die Konfiguration fehlschlagen kann (z. B. ungültige URL, fehlende Pflicht-Felder), gibt bauen() ein Result zurück:

Rust Fallible Builder
pub struct ServerBuilder {
    host: Option<String>,
    port: u16,
    workers: u32,
}

impl ServerBuilder {
    pub fn neu() -> Self {
        ServerBuilder { host: None, port: 8080, workers: 4 }
    }

    pub fn host(mut self, h: impl Into<String>) -> Self {
        self.host = Some(h.into());
        self
    }

    pub fn port(mut self, p: u16) -> Self {
        self.port = p;
        self
    }

    pub fn bauen(self) -> Result<Server, &'static str> {
        let host = self.host.ok_or("Host ist Pflicht")?;
        if self.port < 1024 {
            return Err("Port < 1024 ist privilegiert");
        }
        Ok(Server { host, port: self.port, workers: self.workers })
    }
}

pub struct Server { host: String, port: u16, workers: u32 }

fn main() -> Result<(), &'static str> {
    let s = ServerBuilder::neu()
        .host("0.0.0.0")
        .port(8080)
        .bauen()?;
    println!("Server auf {}:{} mit {} Workers", s.host, s.port, s.workers);
    Ok(())
}

Pflicht-Felder als Option<T> mit Default = Nonebauen() prüft die Pflicht und validiert die Konfiguration.

Typestate-Builder mit Compile-Zeit-Pflicht-Felder

Eine elegantere Alternative: Pflicht-Felder werden über das Typ-System erzwungen.

Rust Typestate-Builder
use std::marker::PhantomData;

pub struct OhneHost;
pub struct MitHost;

pub struct ServerBuilder<S> {
    host: Option<String>,
    port: u16,
    _state: PhantomData<S>,
}

impl ServerBuilder<OhneHost> {
    pub fn neu() -> Self {
        ServerBuilder { host: None, port: 8080, _state: PhantomData }
    }

    pub fn host(self, h: impl Into<String>) -> ServerBuilder<MitHost> {
        ServerBuilder {
            host: Some(h.into()),
            port: self.port,
            _state: PhantomData,
        }
    }
}

impl<S> ServerBuilder<S> {
    pub fn port(mut self, p: u16) -> Self {
        self.port = p;
        self
    }
}

impl ServerBuilder<MitHost> {
    pub fn bauen(self) -> Server {
        Server {
            host: self.host.unwrap(),       // sicher — MitHost garantiert Some
            port: self.port,
        }
    }
}

pub struct Server { host: String, port: u16 }

fn main() {
    // Korrekt:
    let _ = ServerBuilder::neu()
        .host("0.0.0.0")
        .port(8080)
        .bauen();

    // Compile-Fehler — bauen ohne host:
    // let _ = ServerBuilder::neu().port(8080).bauen();
}

bauen() ist nur am ServerBuilder<MitHost> verfügbar. Wer host vergisst, bekommt einen Compile-Fehler, kein Runtime-Error. Klassisches Typestate-Pattern — mehr Aufwand, dafür typsicher.

derive_builder-Crate

Für viele Felder lohnt sich das derive_builder-Crate, das den Builder automatisch generiert:

Rust derive_builder
# // Erfordert Cargo.toml: derive_builder = "0.20"
use derive_builder::Builder;

#[derive(Builder, Debug)]
#[builder(setter(into))]
pub struct Anfrage {
    url: String,
    #[builder(default = "\"GET\".into()")]
    method: String,
    #[builder(default = "5000")]
    timeout_ms: u32,
}

fn main() -> Result<(), AnfrageBuilderError> {
    let a = AnfrageBuilder::default()
        .url("https://example.com")
        .method("POST")
        .build()?;
    println!("{a:?}");
    Ok(())
}

Das Macro generiert AnfrageBuilder-Struct, default()-Konstruktor, Setter pro Feld und build()-Methode. Spart erheblichen Boilerplate-Code bei großen Konfigurations-Strukturen.

Praxis: Builder im echten Code

HTTP-Client

Rust HTTP-Builder
pub struct HttpClientBuilder {
    base_url: Option<String>,
    timeout_ms: u32,
    user_agent: String,
    tls_verify: bool,
}

impl HttpClientBuilder {
    pub fn neu() -> Self {
        HttpClientBuilder {
            base_url: None,
            timeout_ms: 30_000,
            user_agent: "MeineApp/1.0".into(),
            tls_verify: true,
        }
    }

    pub fn base_url(mut self, url: impl Into<String>) -> Self {
        self.base_url = Some(url.into());
        self
    }

    pub fn timeout_sec(mut self, s: u32) -> Self {
        self.timeout_ms = s * 1000;
        self
    }

    pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
        self.user_agent = ua.into();
        self
    }

    pub fn tls_unsicher(mut self) -> Self {
        self.tls_verify = false;
        self
    }

    pub fn bauen(self) -> Result<HttpClient, &'static str> {
        Ok(HttpClient {
            base_url: self.base_url.ok_or("base_url fehlt")?,
            timeout_ms: self.timeout_ms,
            user_agent: self.user_agent,
            tls_verify: self.tls_verify,
        })
    }
}

pub struct HttpClient {
    pub base_url: String,
    pub timeout_ms: u32,
    pub user_agent: String,
    pub tls_verify: bool,
}

fn main() -> Result<(), &'static str> {
    let client = HttpClientBuilder::neu()
        .base_url("https://api.example.com")
        .timeout_sec(15)
        .user_agent("MeineApp/2.0")
        .bauen()?;
    println!("{} ({}s timeout)", client.base_url, client.timeout_ms / 1000);
    Ok(())
}

Klassischer Builder für eine Library-Komponente. Pflicht-Feld via Option + ?-Operator beim Bauen.

Database-Connection-Builder

Rust DB-Builder
pub struct ConnectionBuilder {
    host: String,
    port: u16,
    database: String,
    username: String,
    password: Option<String>,
    pool_size: u32,
    ssl: bool,
}

impl ConnectionBuilder {
    pub fn an(host: impl Into<String>, database: impl Into<String>) -> Self {
        ConnectionBuilder {
            host: host.into(),
            port: 5432,
            database: database.into(),
            username: "postgres".into(),
            password: None,
            pool_size: 10,
            ssl: true,
        }
    }

    pub fn port(mut self, p: u16) -> Self { self.port = p; self }
    pub fn login(mut self, user: &str, pw: &str) -> Self {
        self.username = user.into();
        self.password = Some(pw.into());
        self
    }
    pub fn pool(mut self, size: u32) -> Self { self.pool_size = size; self }
    pub fn ohne_ssl(mut self) -> Self { self.ssl = false; self }

    pub fn verbinden(self) -> Result<Connection, String> {
        println!("Verbinde zu {}:{}/{}", self.host, self.port, self.database);
        Ok(Connection { /* ... */ })
    }
}

pub struct Connection;

an(...) als sprechender Konstruktor mit zwei Pflicht-Argumenten, dann fluent-konfigurieren. verbinden() als finale Aktion, die ein Result liefert.

Email-Builder

Rust Email-Send
pub struct EmailBuilder {
    von: Option<String>,
    an: Vec<String>,
    cc: Vec<String>,
    betreff: String,
    inhalt: String,
    anhaenge: Vec<String>,
}

impl EmailBuilder {
    pub fn neu() -> Self {
        EmailBuilder {
            von: None, an: Vec::new(), cc: Vec::new(),
            betreff: String::new(), inhalt: String::new(),
            anhaenge: Vec::new(),
        }
    }

    pub fn von(mut self, addr: &str) -> Self {
        self.von = Some(addr.into()); self
    }
    pub fn an(mut self, addr: &str) -> Self {
        self.an.push(addr.into()); self
    }
    pub fn cc(mut self, addr: &str) -> Self {
        self.cc.push(addr.into()); self
    }
    pub fn betreff(mut self, s: &str) -> Self {
        self.betreff = s.into(); self
    }
    pub fn inhalt(mut self, s: &str) -> Self {
        self.inhalt = s.into(); self
    }
    pub fn anhang(mut self, pfad: &str) -> Self {
        self.anhaenge.push(pfad.into()); self
    }

    pub fn senden(self) -> Result<(), &'static str> {
        if self.von.is_none() { return Err("von fehlt"); }
        if self.an.is_empty() { return Err("keine Empfänger"); }
        println!("Sende: {}", self.betreff);
        Ok(())
    }
}

fn main() -> Result<(), &'static str> {
    EmailBuilder::neu()
        .von("admin@example.com")
        .an("user@example.com")
        .cc("backup@example.com")
        .betreff("Hallo")
        .inhalt("Test")
        .anhang("/tmp/foo.pdf")
        .senden()?;
    Ok(())
}

Listen-Felder (an, cc, anhaenge) — der Builder akkumuliert Werte über mehrere Aufrufe.

Query-Builder

Rust SQL-Builder
pub struct QueryBuilder {
    tabelle: String,
    where_klauseln: Vec<String>,
    order_by: Option<String>,
    limit: Option<u32>,
}

impl QueryBuilder {
    pub fn select_aus(tabelle: &str) -> Self {
        QueryBuilder {
            tabelle: tabelle.into(),
            where_klauseln: Vec::new(),
            order_by: None,
            limit: None,
        }
    }

    pub fn wo(mut self, klausel: &str) -> Self {
        self.where_klauseln.push(klausel.into()); self
    }

    pub fn sortieren_nach(mut self, feld: &str) -> Self {
        self.order_by = Some(feld.into()); self
    }

    pub fn maximal(mut self, n: u32) -> Self {
        self.limit = Some(n); self
    }

    pub fn bauen(self) -> String {
        let mut q = format!("SELECT * FROM {}", self.tabelle);
        if !self.where_klauseln.is_empty() {
            q.push_str(&format!(" WHERE {}", self.where_klauseln.join(" AND ")));
        }
        if let Some(o) = self.order_by {
            q.push_str(&format!(" ORDER BY {o}"));
        }
        if let Some(l) = self.limit {
            q.push_str(&format!(" LIMIT {l}"));
        }
        q
    }
}

fn main() {
    let q = QueryBuilder::select_aus("users")
        .wo("aktiv = true")
        .wo("alter > 18")
        .sortieren_nach("erstellt_am DESC")
        .maximal(50)
        .bauen();
    println!("{q}");
}

Klassisches Query-Builder-Pattern — alle Database-ORMs in Rust nutzen dieses Schema.

Logger-Konfigurations-Builder

Rust Logger-Config
pub struct LoggerBuilder {
    level: String,
    output: String,
    format_json: bool,
    farbe: bool,
}

impl Default for LoggerBuilder {
    fn default() -> Self {
        LoggerBuilder {
            level: "INFO".into(),
            output: "stderr".into(),
            format_json: false,
            farbe: true,
        }
    }
}

impl LoggerBuilder {
    pub fn level(mut self, l: &str) -> Self { self.level = l.into(); self }
    pub fn nach_datei(mut self, pfad: &str) -> Self {
        self.output = pfad.into(); self
    }
    pub fn json(mut self) -> Self { self.format_json = true; self }
    pub fn ohne_farbe(mut self) -> Self { self.farbe = false; self }

    pub fn bauen(self) -> Logger {
        Logger { /* ... */ }
    }
}

pub struct Logger;

fn main() {
    let _logger = LoggerBuilder::default()
        .level("DEBUG")
        .nach_datei("/var/log/app.log")
        .json()
        .bauen();
}

Default-Trait für sinnvolle Initial-Konfiguration. Builder-Methoden überschreiben gezielt.

Test-Daten-Builder

Rust Test-Fixtures
pub struct UserBuilder {
    id: u64,
    name: String,
    email: String,
    ist_admin: bool,
}

impl UserBuilder {
    pub fn test_user() -> Self {
        UserBuilder {
            id: 1,
            name: "Test User".into(),
            email: "test@example.com".into(),
            ist_admin: false,
        }
    }

    pub fn name(mut self, n: &str) -> Self { self.name = n.into(); self }
    pub fn admin(mut self) -> Self { self.ist_admin = true; self }
    pub fn bauen(self) -> User {
        User { id: self.id, name: self.name, email: self.email, ist_admin: self.ist_admin }
    }
}

pub struct User { pub id: u64, pub name: String, pub email: String, pub ist_admin: bool }

#[test]
fn test_beispiel() {
    let admin = UserBuilder::test_user().admin().bauen();
    let user = UserBuilder::test_user().name("Spezial").bauen();
    assert!(admin.ist_admin);
    assert_eq!(user.name, "Spezial");
}

Test-Builder mit sinnvollen Defaults. Pro Test wird gezielt überschrieben, was abweicht.

Interessantes

Konsumierender Builder ist idiomatischer.

Der self + Self-Stil ist häufiger und kompakter. Aufrufer braucht keine mut-Bindung, Chain-Syntax ist sauberer. &mut self + &mut Self ist nützlich, wenn der Builder mehrfach verwendet wird oder zwischen Funktionen geteilt.

bauen() als finale Methode mit self.

Die abschließende Methode (bauen, build, finish, compile) nimmt typischerweise self und verbraucht den Builder. Sie liefert den finalen Typ — meist Self als anderer Typ, oder Result<Self, Error> bei Fehler-Möglichkeit.

Pflicht-Felder mit Option oder Typestate.

Zwei Patterns für Pflicht-Felder. (1) Option<T> plus Check in bauen() — Runtime-Error bei fehlendem Wert. (2) Typestate mit Phantom-States — Compile-Error bei fehlendem Wert. Typestate ist sicherer, aber aufwendiger.

impl Into für flexible Parameter.

Builder-Setter sollten impl Into<String> oder impl Into<...> akzeptieren — der Aufrufer kann &str, String, Cow<str> übergeben, ohne dass der Builder mehrere Methoden bietet. Standard-Pattern in idiomatischem Rust.

derive_builder-Crate automatisiert die Boilerplate.

Für Builder mit 5+ Feldern lohnt sich das derive_builder-Crate. #[derive(Builder)] generiert den XYZBuilder-Struct mit Setter und build()-Methode automatisch. Konfiguration über #[builder(...)]-Attribute pro Feld.

Listen-Felder akkumulieren über mehrere Setter-Calls.

EmailBuilder::neu().an("a@x").an("b@y") fügt beide zur an-Liste hinzu. Standard-Pattern für Felder, die mehrfach gesetzt werden sollen.

Builder-Methoden sind Self-dokumentierend.

Anfrage::neu(...)::method(...)::timeout(...)::bauen() liest sich fast wie ein Satz. Ein Konsument der API sieht beim Aufruf, was er konfiguriert — viel klarer als positionale Argumente in einem new(...).

Default-Trait für initiale Werte.

Wer einen Builder hat, dessen Default-Werte alle Stdlib-Defaults sind: #[derive(Default)] generiert die default()-Methode. Statt Builder::neu() heißt's dann Builder::default() — beides idiomatisch.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Structs & Methoden

Zur Übersicht