Builder Pattern (Classic GoF + Modern Fluent Style)
A flexible pattern for constructing complex objects step‑by‑step.
TL;DR — Builder Pattern
- Category: Creational Pattern
- Purpose: Construct complex objects step by step.
- Two Common Styles:
- Classic GoF Builder (with Director)
- Modern Fluent Builder (method chaining)
- Use GoF Builder: When the construction process must be reused to create different representations.
- Use the Fluent Builder to avoid telescoping constructors and improve readability.
1. Introduction
The Builder Pattern separates the construction of a complex object from its representation. It allows the same construction process to create different representations.
Why Builder Exists
Simple constructors work well when an object has a few parameters. But when constructors grow like this:
class Computer {
public:
Computer(std::string cpu,
std::string gpu,
int ram,
int storage,
bool wifi,
bool bluetooth);
};
Computer gamingPC("Intel i9", "Nvidia 5060", 128, 1000, yes, yes);
Problems:
- The constructor becomes long and hard to read.
- Parameter order errors are easy to make.
- Adding new options breaks existing code.
This is known as the Telescoping Constructor Problem.
Builder addresses this by:
- Offering named methods (setCPU(), setGPU(), …)
- Allowing step‑by‑step object creation
- Supporting readable, fluent usage
- Making optional parameters easy to omit
- Allowing multiple construction "recipes" (gaming PC, office PC, etc.)
There are two major styles in C++:
- Classic GoF Builder – emphasizes the construction process and role separation.
- Fluent Builder – emphasizes readable configuration and optional parameters.
2. C++ Implementation Example
Classic GoF Builder (With Director)
Classic GoF Builder (With Director)
Product #1 — Computer
class Computer {
public:
std::string cpu;
int ram;
int storage;
void show() const {
std::cout << "Computer: "
<< cpu << ", RAM: "
<< ram << ", Storage: "
<< storage << std::endl;
}
};
Product #2 — ComputerManual (Different Representation)
class ComputerManual {
public:
std::string description;
void show() const {
std::cout << "Manual: "
<< description
<< std::endl;
}
};
Abstract Builder
class Builder {
public:
virtual void buildCPU() = 0;
virtual void buildRAM() = 0;
virtual void buildStorage() = 0;
virtual ~Builder() = default;
};
Concrete Builder — Build a Real Computer
class ComputerBuilder : public Builder {
private:
Computer product;
public:
void buildCPU() override {
product.cpu = "Intel i7";
}
void buildRAM() override {
product.ram = 16;
}
void buildStorage() override {
product.storage = 512;
}
Computer getResult() { return product; }
};
Concrete Builder — Build Manual Instead
class ManualBuilder : public Builder {
private:
ComputerManual manual;
public:
void buildCPU() override {
manual.description += "CPU: Intel i7\n";
}
void buildRAM() override {
manual.description += "RAM: 16GB\n";
}
void buildStorage() override {
manual.description += "Storage: 512GB\n";
}
ComputerManual getResult() { return manual; }
};
Director (Construction Recipe)
class Director {
public:
void construct(Builder& builder) {
builder.buildCPU();
builder.buildRAM();
builder.buildStorage();
}
};
Usage
int main() {
Director director;
ComputerBuilder computerBuilder;
director.construct(computerBuilder);
Computer pc = computerBuilder.getResult();
pc.show();
ManualBuilder manualBuilder;
director.construct(manualBuilder);
ComputerManual manual = manualBuilder.getResult();
manual.show();
}
Notice: Same construction process → Different representations.
Classic GoF Builder (Without Director)
Classic GoF Builder (Without Director)
In many real-world C++ systems, the Director is optional. The client can directly control the construction steps while still preserving the Builder abstraction and multiple representations.
This version still follows the GoF structure:
- Product
- Abstract Builder
- Concrete Builder
But the client controls the build sequence, not the Director.
Product
class House {
public:
std::string foundation;
std::string structure;
std::string roof;
void show() const {
std::cout << "House with "
<< foundation << ", "
<< structure << ", "
<< roof << std::endl;
}
};
Abstract Builder
class HouseBuilder {
public:
virtual void buildFoundation() = 0;
virtual void buildStructure() = 0;
virtual void buildRoof() = 0;
virtual ~HouseBuilder() = default;
};
Concrete Builder
class ConcreteHouseBuilder : public HouseBuilder {
private:
House house;
public:
void buildFoundation() override {
house.foundation = "Concrete Foundation";
}
void buildStructure() override {
house.structure = "Brick Structure";
}
void buildRoof() override {
house.roof = "Tile Roof";
}
House getResult() {
return house;
}
};
Usage (Client Controls Build Order)
int main() {
ConcreteHouseBuilder builder;
// Client directly controls construction sequence
builder.buildFoundation();
builder.buildStructure();
builder.buildRoof();
House house = builder.getResult();
house.show();
return 0;
}
Key Characteristics of GoF Builder (Without Director)
- No Director class.
- Client calls build steps directly.
- Still supports multiple representations by using different ConcreteBuilders.
- Lower abstraction overhead than the Director version.
When to Use a Builder Without a Director
- The Construction process is simple.
- No need to reuse standardized build recipes.
- Client must dynamically control build order.
- The system is not framework-level complex.
Comparison: With vs Without Director
| Aspect | With Director | Without Director |
|---|---|---|
| Build Order Control | Director | Client |
| Recipe Reuse | Strong | Limited |
| Class Count | Higher | Lower |
| Framework Suitability | High | Medium |
Modern Fluent Builder (Method Chaining)
Modern Fluent Builder (Method Chaining) — C++ Example
Modern C++ frequently uses a simpler approach: the Fluent Builder.
The fluent builder focuses on readability and optional configuration. Each setXxx() method returns *this so calls can be chained. The build() method returns the final product.
Product — Computer
#include <iostream>
#include <string>
class Computer {
public:
std::string cpu;
std::string gpu;
int ramGB = 0;
int storageGB = 0;
bool wifi = false;
bool bluetooth = false;
void show() const {
std::cout
<< "Computer: "
<< "CPU=" << cpu
<< ", GPU=" << gpu
<< ", RAM=" << ramGB
<< ", Storage=" << storageGB
<< ", WiFi=" << (wifi ? "Yes" : "No")
<< ", Bluetooth=" << (bluetooth ? "Yes" : "No")
<< std::endl;
}
};
Fluent Builder — ComputerBuilder
Each method returns *this to enable method chaining. The build() function returns the final Computer.
class ComputerBuilder {
private:
Computer computer;
public:
ComputerBuilder& reset() {
computer = Computer{};
return *this;
}
ComputerBuilder& setCPU(const std::string& value) {
computer.cpu = value;
return *this;
}
ComputerBuilder& setGPU(const std::string& value) {
computer.gpu = value;
return *this;
}
ComputerBuilder& setRAM(int value) {
computer.ramGB = value;
return *this;
}
ComputerBuilder& setStorage(int value) {
computer.storageGB = value;
return *this;
}
ComputerBuilder& enableWiFi(bool value = true) {
computer.wifi = value;
return *this;
}
ComputerBuilder& enableBluetooth(bool value = true) {
computer.bluetooth = value;
return *this;
}
Computer build() {
// Optional validation logic:
// if (computer.cpu.empty()) throw std::runtime_error("CPU required");
return computer;
}
};
Usage (Client Code)
int main() {
Computer gamingPC = ComputerBuilder{}
.setCPU("Intel i9")
.setGPU("RTX 4090")
.setRAM(32)
.setStorage(2000)
.enableWiFi()
.enableBluetooth()
.build();
gamingPC.show();
// Reusing builder
ComputerBuilder builder;
Computer officePC = builder.reset()
.setCPU("Intel i5")
.setGPU("Integrated GPU")
.setRAM(16)
.setStorage(512)
.enableWiFi()
.build();
officePC.show();
return 0;
}
Optional — Fluent Builder Interface
Most Fluent Builders do not require an abstract interface. However, if multiple builder implementations are needed, you may introduce an interface like this:
class IComputerBuilder {
public:
virtual ~IComputerBuilder() = default;
virtual IComputerBuilder& setCPU(const std::string&) = 0;
virtual IComputerBuilder& setGPU(const std::string&) = 0;
virtual IComputerBuilder& setRAM(int) = 0;
virtual IComputerBuilder& setStorage(int) = 0;
virtual Computer build() = 0;
};
The concrete builder would implement this interface and still return *this (as a reference to the interface).
Key Characteristics
- Readable configuration-style API.
- Solves the telescoping constructor problem.
- No Director required.
- Common in modern C++ application-level code.
Fluent Builder + Immutable Computer (Advanced Modern C++ Design)
Fluent Builder + Immutable Computer (Advanced Modern C++ Design)
A common advanced design is to combine a fluent builder with an immutable product. This prevents partially-initialized states and reduces bugs caused by global mutations after construction.
- Immutable Product: fields cannot be modified after construction.
- Builder: collects configuration, validates it, then constructs the immutable product.
- Best for: configuration objects, request objects, security-sensitive settings, multi-threaded read-only sharing.
Each step returns a new builder with an updated state.
Useful for:
- thread safety
- parallel building
- functional programming style
Immutable Product — Computer
The product has private fields and only provides getters. The constructor is private to enforce builder-only construction.
#include <string>
#include <stdexcept>
class ImmutableComputer {
private:
const std::string cpu_;
const std::string gpu_;
const int ramGB_;
const int storageGB_;
const bool wifi_;
const bool bluetooth_;
// Only the builder can call this constructor
ImmutableComputer(std::string cpu,
std::string gpu,
int ramGB,
int storageGB,
bool wifi,
bool bluetooth)
: cpu_(std::move(cpu)),
gpu_(std::move(gpu)),
ramGB_(ramGB),
storageGB_(storageGB),
wifi_(wifi),
bluetooth_(bluetooth) {}
public:
// Getters only (read-only)
const std::string& cpu() const { return cpu_; }
const std::string& gpu() const { return gpu_; }
int ramGB() const { return ramGB_; }
int storageGB() const { return storageGB_; }
bool wifi() const { return wifi_; }
bool bluetooth() const { return bluetooth_; }
// Builder needs access to the private constructor
class Builder;
};
Fluent Builder with Validation
The builder stores the mutable configuration, validates it, and then constructs the immutable object in build().
class ImmutableComputer::Builder {
private:
std::string cpu;
std::string gpu = "Integrated GPU";
int ramGB = 8;
int storageGB = 256;
bool wifi = false;
bool bluetooth = false;
public:
Builder& setCPU(const std::string& value) {
cpu = value;
return *this;
}
Builder& setGPU(const std::string& value) {
gpu = value;
return *this;
}
Builder& setRAM(int value) {
ramGB = value;
return *this;
}
Builder& setStorage(int value) {
storageGB = value;
return *this;
}
Builder& enableWiFi(bool value = true) {
wifi = value;
return *this;
}
Builder& enableBluetooth(bool value = true) {
bluetooth = value;
return *this;
}
ImmutableComputer build() const {
// Validation examples:
if (cpu.empty()) {
throw std::runtime_error("CPU is required.");
}
if (ramGB <= 0 || storageGB <= 0) {
throw std::runtime_error("RAM and Storage must be positive.");
}
return ImmutableComputer(cpu, gpu, ramGB, storageGB, wifi, bluetooth);
}
};
Usage
#include <iostream>
int main() {
try {
ImmutableComputer pc = ImmutableComputer::Builder{}
.setCPU("Intel i9")
.setGPU("RTX 4090")
.setRAM(32)
.setStorage(2000)
.enableWiFi()
.enableBluetooth()
.build();
std::cout << "ImmutableComputer built: CPU=" << pc.cpu()
<< ", RAM=" << pc.ramGB()
<< ", WiFi=" << (pc.wifi() ? "Yes" : "No")
<< std::endl;
// pc.cpu_ = "AMD"; // NOT allowed (private + const)
}
catch (const std::exception& ex) {
std::cerr << "Build failed: " << ex.what() << std::endl;
}
}
Why Immutable + Builder is Valuable
- Safety: prevents accidental mutation after construction.
- Validity: the builder can enforce invariants before object creation.
- Concurrency: immutable objects are easier to share across threads (read-only).
- Maintainability: construction rules stay centralized in the builder.
3. Correct Use vs Incorrect Use
✔ Correct Uses (When Builder is a Good Idea)
- Complex Objects with Many Optional Fields
Configuration objects, UI widgets, Cars, HTTP requests, scenes, game entities. - Objects with “Recipes” or Variants
e.g., building different PC types (Gaming PC vs Workstation) using different Builders. - Multi‑step Construction
When construction involves ordering, validation steps, or dependencies. - Improving Readability
Fluent builders create expressive code that reads like a sentence:auto pizza = PizzaBuilder().addCheese().addSauce().addPepperoni().build() - Avoiding Telescoping Constructors
Multiple constructors with varying parameters become unmanageable.
✘ Incorrect Uses (Anti‑Patterns and Overuse)
- When a Single Constructor Is Enough
If the object is simple:
Using a builder is unnecessary.C++Point p{10, 20}; - When the object is immutable or trivial
For small structs or POD types, the builder adds overhead without value. - Overengineering
If your “builder” only sets two fields and then calls build(), stop. Use a constructor or simple factory. - Requiring the Director to do everything
If the Director becomes a “god object,” the design has drifted away from the original intent.
3. Comparison: GoF With Director vs GoF Without Director vs Fluent Builder
Although all three are called “Builder,” they address different architectural pain points:
- GoF Builder (With Director): Separates the construction algorithm (recipe) from the build steps and product representation.
- GoF Builder (Without Director): Keeps the Builder abstraction, but the client controls the build sequence.
- Fluent Builder: A modern style optimized for configuration readability and avoiding “telescoping constructors.”
| Aspect | Classic GoF Builder (With Director) | Classic GoF Builder (Without Director) | Modern Fluent Builder (Method Chaining) |
|---|---|---|---|
| Who controls the build order? | Director | Client | Client |
| Builder abstraction/interface? | Common (yes) | Common (yes) | Often no (optional) |
| Recipe reuse (standard/premium/minimal) | Strong | Limited/manual reuse | Weak (typically client-driven) |
| Different representations from the same process | Best (multiple ConcreteBuilders) | Good (multiple ConcreteBuilders) | Usually not a goal |
| Complexity/class count | High | Medium | Low |
| Typical usage | Frameworks, libraries, standardized build pipelines | Mid-size systems needing Builder abstraction without formal recipes | App-level configuration APIs (options, requests, settings) |
4. When to Use Each Variant
Use Classic GoF Builder (With Director) when:
- You need reusable construction recipes (standard/minimal/premium builds).
- You want the same construction process to create different representations (e.g., Product and ProductManual).
- Build steps follow a strict order, have dependencies, or follow a standardized pipeline.
- You are designing a framework or a library API.
Use Classic GoF Builder (Without Director) when:
- You still want the Builder abstraction (multiple builders / multiple outputs).
- The build process is not complex enough to justify a separate Director.
- The client must dynamically determine the build order (conditional steps).
Use Modern Fluent Builder (Method Chaining) when:
- You have many optional parameters and want to avoid the telescoping constructor problem.
- You want a readable configuration style API.
- You do not need formal recipe reuse or multiple representations.
- You want minimal boilerplate and modern C++ ergonomics.
5. Practical Takeaway
- GoF + Director = construction algorithm (recipe) abstraction (powerful but heavier).
- GoF without Director = keep the Builder abstraction; the client controls the sequence (balanced).
- Fluent Builder = configuration readability and convenience (most common in modern app code).
6. Engineering Insight
In real-world C++ systems:
- Fluent Builder is more common in application-level code.
- Classic Builder is more commonly used in frameworks, libraries, and large architectural designs.
Choosing the correct variant depends on whether your main problem is:
- "Too many constructor parameters" → Use Fluent Builder.
- "Same build algorithm, different outputs" → Use Classic Builder.
7. Summary
- A builder constructs complex objects step-by-step.
- Classic Builder emphasizes the separation of construction and representation.
- Fluent Builder emphasizes readability and optional configuration.
- Do not over-engineer small classes.
- Use the pattern only when complexity justifies it.