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.
}

Das Pattern führt schnell ins Chaos. Bei drei optionalen Konfigurations-Werten hast du potentiell acht Konstruktor-Varianten (2³). Bei fünf optionalen Werten sind es 32. Jede neue Option verdoppelt den Aufwand. Und selbst wenn du dich auf wenige Konstruktoren beschränkst, ist die Aufruf-Syntax mit positional Parametern unleserlich: Anfrage::new("...", "POST", 5000, vec![], vec![]) lässt nicht erkennen, was die Zahlen oder leeren Vecs bedeuten.

Der Builder löst beides: schrittweiser Aufbau mit benannten Konfigurations-Methoden. Aus new(url, method, timeout, headers, body) wird Builder::neu(url).method("POST").timeout(5000).bauen() — selbsterklärend, nur die wirklich benötigten Optionen werden gesetzt, alles andere bleibt beim Default.

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);
}

Vier Eigenschaften charakterisieren den konsumierenden Builder-Stil.

mut self als Receiver: jede Konfigurations-Methode übernimmt den Builder per Move, ändert ein Feld, und gibt ihn wieder her. Der Builder wird also bei jeder Methode physisch durchgereicht — was Move-Optimierungen des Compilers oft komplett wegoptimiert.

Self-Return: die Methode gibt den modifizierten Builder als ihr eigenes Resultat zurück. Damit kann die nächste Methode in der Chain direkt darauf operieren — obj.foo().bar().baz()-Syntax wird möglich.

bauen(self) konsumiert: die finale Methode nimmt den Builder per Move und transformiert ihn in den eigentlichen Ziel-Typ. Der Builder existiert danach nicht mehr; das spart einen separaten Drop-Schritt und macht die Konstruktions-Logik effizient.

Aufrufer braucht keine mut-Bindung: weil der Builder by-value durch die Chain läuft, kann er als rvalue-Expression aufgebaut werden — kein let mut builder = ...; builder.foo(); builder.bar();-Boilerplate. Das ist der ergonomische Hauptvorteil des Stils.

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"));
}

Der mutable Builder-Stil ist die Alternative für Situationen, in denen der Builder nach dem Bauen weiter genutzt werden soll — etwa als wiederverwendbare Konfigurations-Vorlage. Vier Unterschiede zum konsumierenden Stil:

&mut self als Receiver: die Methoden nehmen eine mutable Referenz, statt den Builder zu konsumieren. Der Aufrufer behält das Objekt.

&mut Self-Return für Chaining: jede Methode gibt eine mutable Self-Referenz zurück, sodass die Chain weiterläuft. Bei der nächsten Iteration wird per Reborrow gearbeitet.

Aufrufer braucht mut-Bindung: let mut b = UrlBuilder::neu(...) — sonst kann der Aufrufer keine &mut self-Methoden anrufen.

bauen(&self) mit Shared Borrow: die finale Methode konsumiert nicht, sondern liest den Builder. Damit kannst du nach dem Bauen weiter konfigurieren oder ein zweites Mal bauen mit anderen Parametern.

Der mutable Stil ist seltener, aber für Konfigurations-Templates oder wiederverwendbare Builder die richtige Wahl.

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

Die Wahl zwischen den beiden Stilen ist eine API-Designentscheidung, die du je nach Anwendungsfall triffst. Konsumierend ist häufiger und prägnanter — die Mehrheit der Stdlib- und Library-Builder folgt diesem Stil. Wenn du einen Builder einmalig zur Konstruktion brauchst, ist das die richtige Wahl.

Mutable wird nützlich, wenn du den Builder als lebendes Konfigurations-Objekt verstehst: ein Template, das mehrfach gebaut wird (mit jeweils anderen kleinen Anpassungen), ein gemeinsamer Builder, der von verschiedenen Funktionen schrittweise konfiguriert wird, oder ein Builder, der die Konfiguration zur Inspektion ausliest, bevor du bauen aufrufst.

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, String> {
        let host = self.host.ok_or_else(|| String::from("Host ist Pflicht"))?;
        if self.port < 1024 {
            return Err(String::from("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<(), String> {
    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.

Das Beispiel nutzt Generics (ServerBuilder<S>) und PhantomData — beides wird in den Kapiteln zu Generics und Lifetimes/Variance ausführlich behandelt. Der Code zeigt das Muster zum Wiederfinden; die Mechanik klärt sich später.

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, String> {
        Ok(HttpClient {
            base_url: self.base_url.ok_or_else(|| String::from("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<(), String> {
    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<(), String> {
        if self.von.is_none() { return Err(String::from("von fehlt")); }
        if self.an.is_empty() { return Err(String::from("keine Empfänger")); }
        println!("Sende: {}", self.betreff);
        Ok(())
    }
}

fn main() -> Result<(), String> {
    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