Builder

Learning Objectives

Build complex objects step by step with validation in build().

Replace telescoping constructors with a fluent builder.

Produce immutable results and enforce required fields.

Prerequisites: Modules 5, 11.

Intuition and Definition

Intent: separate the construction of a complex object from its representation, so the same construction process can create valid objects step by step.

Problem: an object has many parameters, several optional, with cross-field rules ("if TLS is on, a certificate path is required"). Constructors with 8 parameters are unreadable and overloads explode ("telescoping constructors").

Participants: Builder (accumulates settings, validates in build()), Product (often immutable), optionally a Director that runs a standard building sequence.

Why It Matters

Builders make call sites self-documenting and guarantee that only valid, complete objects escape.

Syntax and Language Notes

# Python: keyword arguments with defaults often replace a builder
conn = Connection(host="db", port=5432, tls=True, timeout_s=5)
// C#: object initializers + required members (C# 11)
var req = new Request { Url = "https://api", TimeoutSeconds = 5 };

Complete Example

#include <expected>
#include <iostream>
#include <optional>
#include <string>
#include <vector>

class HttpRequest {                                   // immutable product
public:
    const std::string method, url;
    const std::vector<std::pair<std::string, std::string>> headers;
    const int timeoutMs;
    const std::optional<std::string> body;
private:
    friend class HttpRequestBuilder;
    HttpRequest(std::string m, std::string u, std::vector<std::pair<std::string, std::string>> h, int t, std::optional<std::string> b)
        : method(std::move(m)), url(std::move(u)), headers(std::move(h)), timeoutMs(t), body(std::move(b)) {}
};

class HttpRequestBuilder {
    std::string method_ = "GET", url_;
    std::vector<std::pair<std::string, std::string>> headers_;
    int timeoutMs_ = 5000;
    std::optional<std::string> body_;
public:
    HttpRequestBuilder& url(std::string u) { url_ = std::move(u); return *this; }
    HttpRequestBuilder& post(std::string b) { method_ = "POST"; body_ = std::move(b); return *this; }
    HttpRequestBuilder& header(std::string k, std::string v) { headers_.emplace_back(std::move(k), std::move(v)); return *this; }
    HttpRequestBuilder& timeoutMs(int 
…
POST https://fleet.example/api/v1/telemetry (1 header, 3000 ms)
rejected: url must start with https://
rejected: timeout must be 1..60000 ms

Step-by-Step Walkthrough

Each setter records one choice and returns the builder for chaining.

build() checks every rule (scheme, timeout range, POST needs a body) and returns std::expected — invalid requests never exist.

The product's fields are const and its constructor is private, so it can only come from the builder and can't change afterwards.

Common Mistakes

Builders that skip validation — just a verbose constructor.

Mutable products whose setters bypass the builder's rules.

Builders for simple objects with two or three fields — a constructor or designated initializers are clearer.

Reusing a builder after build() without documenting whether it resets.

Trade-offs and Alternatives

Named/keyword arguments (Python, C#, Kotlin) Optional parameters without cross-field rules

C++20 designated initializers {.url = "...",.timeoutMs = 3000} A few named configurations Step builder (one interface per step)

Required fields enforced by the type system

Real-World Use

HTTP clients (HttpRequest.newBuilder() in Java), protobuf message builders, StringBuilder, test-data builders (aCustomer().withAddress(...).build()), and CAN frame or diagnostic request builders.

Practice

Q1. (Easy) What problem does Builder solve that constructors don't?

Answer: Many optional parameters and cross-field validation without telescoping overloads or unreadable argument lists.

Q2. (Medium) Where should validation live in a builder?

Answer: In build(), where all fields are known, so cross-field rules can be checked once.

Q3. (Advanced) Write a test-data builder for an Order with sensible defaults so tests specify only what they care about.

#include <iostream>
#include <string>
struct Order { std::string customer; int items; bool express; double total; };
class OrderBuilder {
    Order o_{"test-customer", 1, false, 10.0};        // defaults valid for any test
public:
    OrderBuilder& express() { o_.express = true; return *this; }
    OrderBuilder& items(int n) { o_.items = n; o_.total = 10.0 * n; return *this; }
    Order build() const { return o_; }
};
int main() {
    Order o = OrderBuilder().items(3).express().build();   // the test states only what matters
    std::cout << o.customer << " " << o.items << " express=" << o.express << " total=" << o.total << "\n";
}
test-customer 3 express=1 total=30

Interview Question

Q1. "How would you guarantee at compile time that a builder's required fields are set?"

Answer: Use a *step builder*: each step returns a different interface type exposing only the next required setter (UrlStep → MethodStep → OptionalStep{header, timeout, build}), so build() is unreachable until required fields are provided. In C++ you can also encode state in template parameters (Builder<HasUrl, HasMethod> with build() constrained by requires). The trade-off is many small types.

Quick Quiz

Q1. What does build() typically return?

Answer: The finished (often immutable) product — or an error/expected when validation fails.

Q2. Name a language feature that often makes builders unnecessary.

Answer: Named/keyword arguments with defaults.

Q3. What is a Director?

Answer: An optional object that runs a standard sequence of builder steps.

Summary

Builder assembles complex objects stepwise and validates once in build().

Pair it with immutable products; skip it for simple objects.

Related: Modules 5, 11, 64 (test-data builders).