
Start with the design patterns overview · Part 02 of 23 · Creational patterns
Construct a complex object step by step while allowing different final representations. This article follows the example in my 23 Design Patterns repository, connecting the problem, participating classes, Python and C++20 implementations, and the trade-offs that decide whether to use it.
The Problem
A request has an endpoint, a timeout and a retry option; positional arguments become hard to read as options grow.
Naive Solution
Request request{"/orders", 5, true}; // what does true mean?
Why It Becomes a Problem
The constructor works, but calls with several integers and booleans hide intent and make swapped arguments hard to notice.
The Idea
Keep construction state in RequestBuilder. Named methods collect choices; build checks required values and returns a Request by value.
Reading the illustration
A restaurant uses the same sequence—bread, filling, sauce, finish—but different cooks can produce different sandwiches from those steps.
In the repository example, the same design idea addresses this software problem: A request has an endpoint, a timeout and a retry option; positional arguments become hard to read as options grow.
Structure
Diagram · Python source · C++20 source
Client --> RequestBuilder --> Request
Participants
RequestBuilder stores temporary choices and validates them. Request owns the finished values. The client chooses the order of optional steps.
Canonical roles in this example:
Product— The contract of an object returned by creation code. Here:Request.fluent interface— An interface shaped to read as a chain of calls; it does not by itself imply Builder. Here:RequestBuilder.endpoint().timeout().retry().constructor— The special operation that initializes a new class instance. Here:Request::Request.
Python Example
The complete Python source is shown first.
class Request:
def __init__(self, endpoint, timeout=30, retry=False):
self.endpoint = endpoint
self.timeout = timeout
self.retry = retry
def describe(self):
print(f"{self.endpoint} timeout={self.timeout} retry={self.retry}")
class RequestBuilder:
def __init__(self):
self.endpoint_value = ""
self.timeout_value = 30
self.retry_value = False
def endpoint(self, value):
self.endpoint_value = value
return self
def timeout(self, seconds):
self.timeout_value = seconds
return self
def retry(self, enabled):
self.retry_value = enabled
return self
def build(self):
if not self.endpoint_value or self.timeout_value <= 0:
raise ValueError("Invalid request")
return Request(self.endpoint_value, self.timeout_value, self.retry_value)
def main():
RequestBuilder().endpoint("/orders").timeout(5).retry(True).build().describe()
try:
RequestBuilder().build()
except ValueError:
print("Invalid request rejected")
try:
RequestBuilder().endpoint("/orders").timeout(0).build()
except ValueError:
print("Zero timeout rejected")
if __name__ == "__main__":
main()
Python Output
/orders timeout=5 retry=True
Invalid request rejected
Zero timeout rejected
Code Walkthrough
RequestBuilder stores temporary choices and validates them. Request owns the finished values. The client chooses the order of optional steps.
Start at the final call in the Python example. Follow the middle role in the diagram and compare how the C++20 version handles the same responsibility.
C++20 Example
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
class Request {
std::string endpoint_;
int timeout_;
bool retry_;
public:
Request(std::string endpoint, int timeout, bool retry)
: endpoint_(std::move(endpoint)), timeout_(timeout), retry_(retry) {}
void describe() const {
std::cout << endpoint_ << " timeout=" << timeout_ << " retry=" << retry_ << '\n';
}
};
class RequestBuilder {
std::string endpoint_;
int timeout_ = 30;
bool retry_ = false;
public:
RequestBuilder& endpoint(std::string value) { endpoint_ = std::move(value); return *this; }
RequestBuilder& timeout(int seconds) { timeout_ = seconds; return *this; }
RequestBuilder& retry(bool enabled) { retry_ = enabled; return *this; }
Request build() const {
if (endpoint_.empty() || timeout_ <= 0) throw std::invalid_argument("Invalid request");
return Request{endpoint_, timeout_, retry_};
}
};
int main() {
const auto request = RequestBuilder{}.endpoint("/orders").timeout(5).retry(true).build();
request.describe();
try { static_cast<void>(RequestBuilder{}.build()); }
catch (const std::invalid_argument&) { std::cout << "Invalid request rejected\n"; }
}
C++20 Output
/orders timeout=5 retry=1
Invalid request rejected
When to Use
Use it for objects with many independent options or a meaningful validation boundary.
Use cases
HTTP request configuration and test fixture assembly fit; this example performs no network request.
When NOT to Use
Avoid it for two obvious constructor arguments; a small aggregate with named fields may be clearer.
Advantages
Call sites explain the choices, and invalid builder state can be rejected before producing a result.
Trade-offs
There is an extra type to maintain. This Request constructor remains public, so production invariants would also need constructor validation or restricted access.
Related Patterns
Factory Method · Abstract Factory
Common Confusion
Factory Method chooses the concrete product inside an inherited workflow. Builder assembles the configuration of a result over several calls.
Terms to Remember
Builder— Assemble a configured object through named steps before producing the result.Product— The contract of an object returned by creation code. Example:Request.fluent interface— An interface shaped to read as a chain of calls; it does not by itself imply Builder. Example:RequestBuilder.endpoint().timeout().retry().constructor— The special operation that initializes a new class instance. Example:Request::Request.
Interview Vocabulary
object creation— Choosing a concrete type and establishing an object’s initial values and lifetime.separation of concerns— Keeping distinct kinds of responsibility apart so they can change independently.single responsibility— Keep a module focused on one coherent reason to change.
Interview Question
Does a fluent interface automatically make something a Builder? Explain where construction ends.
Mini Challenge
Reject timeouts above 120 and demonstrate both the boundary value and the first rejected value.
Compare the two versions
Named Python arguments often make a Builder unnecessary. This example keeps separate construction steps to show the intent. build creates a fresh Request; as in C++, calling the public Request constructor directly bypasses Builder validation. A GoF Director is optional here, and the example builds one representation. The two examples express the same pattern responsibility; compare their setup and output before changing an input.
Check yourself
- What happens if a caller bypasses build and calls Request directly?
- When would the naive solution on this page be easier to maintain? Give a concrete example.
- Change one input in the Python example. Predict the output and explain which responsibility handles the change.
Run and explore the example
The Python code comes from python/main.py, with expected output. The C++20 code comes from creational/builder/cpp/main.cpp. Follow the repository’s C++20 build instructions to compile it and compare the result with expected.txt. The output demonstrates this example’s behavior; it does not cover every input or the challenge above.
The source example and adapted explanation are © 2026 Sanfor2004, provided under the MIT license. The sketchbook cover is an illustration preserved from this site’s original pattern lessons.
Continue the series
- Overview: all 23 patterns, their categories, and how to choose
- Previous: Abstract Factory (Creational Pattern)
- Next: Factory Method (Creational Pattern)