Constructors and Builders
Rule
Use new or try_new for required fields, add a builder for substantial optional configuration, and use typestate builders only for important required steps.
Why
Constructors keep invariants close to a type. Builders improve named optional configuration but add API surface and should earn that cost.
Do
- Use
newfor infallible construction from required values. - Use
try_newwhen construction validates or can fail; reserveparseforFromStr-backed text parsing. - Follow ownership policy for constructor parameters.
- Keep validation inside the constructor or
buildmethod. - Implement
Defaultonly for an obvious, useful value. - Use a builder when defaults or optional fields would otherwise produce boolean flags and repeated
Nonearguments. - Prefer consuming setters for owned configuration builders.
- Use typestate only when a missing or out-of-order step would violate an important invariant.
Avoid
- Do not add a builder for every struct.
- Do not let
newpanic on caller-provided input. - Do not use long positional constructors with boolean flags.
- Do not encode ordinary optional settings with typestate.
- Do not provide a surprising, invalid, or environment-dependent
Default.
Public API Notes
Adding a required constructor parameter is normally breaking. Optional builder methods are easier to add compatibly to published APIs.
Example
#![allow(unused)]
fn main() {
#[derive(Clone, Debug)]
pub struct ClientOptions {
endpoint: String,
timeout: Duration,
}
impl ClientOptions {
pub fn new(endpoint: impl Into<String>) -> Self {
Self {
endpoint: endpoint.into(),
timeout: Duration::from_secs(30),
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
}
}
Exceptions
- Use struct literals for intentional plain data.
- Use
&mut selfsetters when callers must reuse a builder or an existing API establishes that style. - Use a builder crate only when builder-heavy code justifies the dependency.