Builder Pattern
Build complex configuration or domain objects step-by-step with defaults, fluent setters, and validation in build().
Recipe
let client = HttpClient::builder()
.timeout(Duration::from_secs(10))
.build()?;When to reach for this: Many optional fields or validation that should run once at construction.
Working Example
#[derive(Default)]
struct ConfigBuilder {
port: Option<u16>,
host: Option<String>,
}
impl ConfigBuilder {
pub fn port(mut self, port: u16) -> Self { self.port = Some(port); self }
pub fn host(mut self, host: impl Into<String>) -> Self { self.host = Some(host.into()); self }
pub fn build(self) -> Result<Config, &'static str> {
Ok(Config {
port: self.port.ok_or("port required")?,
host: self.host.unwrap_or_else(|| "localhost".into()),
})
}
}What this demonstrates:
- Only
Configis public after successful build - Defaults live in builder, not scattered at call sites
build()returnsResultfor validation errors- Chainable consuming
selfmethods
Deep Dive
Derive with typed-builder or bon for large structs. For async clients, builder might produce Client that connects on build().await.
Gotchas
- Builder exposed but struct fields public - bypass validation. Fix: private fields on product type.
- Clone-heavy builder - slow. Fix:
mut selfchain without cloning strings until build. - Infinite optional fields without defaults doc - confusion. Fix: document defaults in rustdoc.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Config::from_env | 12-factor apps | Library API |
| Struct literal | ≤3 fields | Many optional fields |
serde + validate | File-based config | Programmatic construction |
FAQs
builder vs Default?
Default gives empty object; builder adds validation and staged setup.
async build?
build(self) -> impl Future<Output = Result<Client>> for connection setup.
required fields?
Option in builder until build(); or separate new(required) + optional setters.
clap integration?
Flatten clap args into builder or build Config from matches.
test builders?
ConfigBuilder::default().port(0).build().unwrap() in tests.
typestate builder?
Compile-time phase separation; see typestate pattern.
derive_builder pitfalls?
Regenerate on field changes; review generated API surface.
immutable builder?
&mut self setters if sharing builder across threads (rare).
partial build?
Do not expose half-built product; only build() returns product.
document chain order?
Usually order-independent; document if setters interact.
Related
- Typestate Pattern - staged builders
- Rust Patterns Basics - overview
- Configuration & Feature Design
- Rust Patterns Best Practices
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.