Builder Design Pattern
Context
In software engineering, we often need to construct complex objects step-by-step. Imagine building a vacation planner for a theme park. Park guests can choose a hotel, various types of admission tickets, make restaurant reservations, and book special events. The exact components of each vacation plan will vary wildly depending on the guest’s needs (e.g., local resident vs. out-of-state visitor).
Problem
When an object requires multi-step construction or has many optional parameters, putting all the initialization logic into a single constructor or factory method becomes unwieldy.
- Coupled Construction: The algorithm for creating the complex object becomes tightly coupled to the parts that make up the object and how they are assembled.
- Incomplete Objects: If construction steps are exposed directly to the client, there’s a risk of the client using a partially constructed, invalid object.
- Telescoping Constructors: You might end up with a massive constructor with dozens of parameters, most of which are null or default values for any given instance. (Note: this problem is the primary motivation for the closely related fluent builder variant popularized by Joshua Bloch in Effective Java — see the variant note below.)
Solution
The Builder Pattern separates the construction of a complex object from its representation so that the same construction process can create different representations. It encapsulates the way a complex object is built and allows it to be constructed incrementally.
The pattern involves four main participants:
- Builder: Specifies an abstract interface for creating the various parts of a
Productobject. - ConcreteBuilder: Constructs and assembles the parts by implementing the
Builderinterface. It defines and tracks the internal representation it creates and provides a method for retrieving the finished product. - Director: Constructs the object using the abstract
Builderinterface. It dictates the exact step-by-step construction sequence. - Product: Represents the complex object under construction.
UML Role Diagram
UML Example Diagram
Code Example
This example builds a vacation plan through one specific construction sequence. The director controls the steps; the concrete builder controls the internal representation of the finished plan. (Different Director implementations could encode different sequences over the same VacationBuilder interface.)
Teaching example: These snippets are intentionally small. They show one reasonable mapping of the pattern roles, not a drop-in architecture. In production, always tailor the pattern to the concrete context: lifecycle, ownership, error handling, concurrency, dependency injection, language idioms, and team conventions.
import java.util.ArrayList;
import java.util.List;
final class VacationPlanner {
private final List<String> itinerary = new ArrayList<>();
void addItem(String item) {
itinerary.add(item);
}
void showPlan() {
itinerary.forEach(System.out::println);
}
}
interface VacationBuilder {
void buildDay(String date);
void addHotel(String date, String hotelName);
void addTickets(String eventName);
VacationPlanner getVacationPlanner();
}
final class PatternslandBuilder implements VacationBuilder {
private final VacationPlanner planner = new VacationPlanner();
public void buildDay(String date) {
planner.addItem("Day started on " + date);
}
public void addHotel(String date, String hotelName) {
planner.addItem("Hotel '" + hotelName + "' booked for " + date);
}
public void addTickets(String eventName) {
planner.addItem("Tickets purchased for '" + eventName + "'");
}
public VacationPlanner getVacationPlanner() {
return planner;
}
}
final class Director {
private final VacationBuilder builder;
Director(VacationBuilder builder) {
this.builder = builder;
}
void constructPlanner() {
builder.buildDay("August 10");
builder.addHotel("August 10", "Grand Facadian");
builder.addTickets("Patterns on Ice");
}
}
public class Demo {
public static void main(String[] args) {
PatternslandBuilder builder = new PatternslandBuilder();
new Director(builder).constructPlanner();
builder.getVacationPlanner().showPlan();
}
}
#include <iostream>
#include <string>
#include <vector>
class VacationPlanner {
public:
void addItem(const std::string& item) {
itinerary_.push_back(item);
}
void showPlan() const {
for (const auto& item : itinerary_) {
std::cout << item << "\n";
}
}
private:
std::vector<std::string> itinerary_;
};
class VacationBuilder {
public:
virtual ~VacationBuilder() = default;
virtual void buildDay(const std::string& date) = 0;
virtual void addHotel(const std::string& date, const std::string& hotelName) = 0;
virtual void addTickets(const std::string& eventName) = 0;
virtual VacationPlanner& getVacationPlanner() = 0;
};
class PatternslandBuilder : public VacationBuilder {
public:
void buildDay(const std::string& date) override {
planner_.addItem("Day started on " + date);
}
void addHotel(const std::string& date, const std::string& hotelName) override {
planner_.addItem("Hotel '" + hotelName + "' booked for " + date);
}
void addTickets(const std::string& eventName) override {
planner_.addItem("Tickets purchased for '" + eventName + "'");
}
VacationPlanner& getVacationPlanner() override {
return planner_;
}
private:
VacationPlanner planner_;
};
class Director {
public:
explicit Director(VacationBuilder& builder) : builder_(builder) {}
void constructPlanner() {
builder_.buildDay("August 10");
builder_.addHotel("August 10", "Grand Facadian");
builder_.addTickets("Patterns on Ice");
}
private:
VacationBuilder& builder_;
};
int main() {
PatternslandBuilder builder;
Director director(builder);
director.constructPlanner();
builder.getVacationPlanner().showPlan();
}
from abc import ABC, abstractmethod
class VacationPlanner:
def __init__(self) -> None:
self.itinerary: list[str] = []
def add_item(self, item: str) -> None:
self.itinerary.append(item)
def show_plan(self) -> None:
for item in self.itinerary:
print(item)
class VacationBuilder(ABC):
@abstractmethod
def build_day(self, date: str) -> None:
pass
@abstractmethod
def add_hotel(self, date: str, hotel_name: str) -> None:
pass
@abstractmethod
def add_tickets(self, event_name: str) -> None:
pass
@abstractmethod
def get_vacation_planner(self) -> VacationPlanner:
pass
class PatternslandBuilder(VacationBuilder):
def __init__(self) -> None:
self._planner = VacationPlanner()
def build_day(self, date: str) -> None:
self._planner.add_item(f"Day started on {date}")
def add_hotel(self, date: str, hotel_name: str) -> None:
self._planner.add_item(f"Hotel '{hotel_name}' booked for {date}")
def add_tickets(self, event_name: str) -> None:
self._planner.add_item(f"Tickets purchased for '{event_name}'")
def get_vacation_planner(self) -> VacationPlanner:
return self._planner
class Director:
def __init__(self, builder: VacationBuilder) -> None:
self._builder = builder
def construct_planner(self) -> None:
self._builder.build_day("August 10")
self._builder.add_hotel("August 10", "Grand Facadian")
self._builder.add_tickets("Patterns on Ice")
builder = PatternslandBuilder()
Director(builder).construct_planner()
builder.get_vacation_planner().show_plan()
class VacationPlanner {
private readonly itinerary: string[] = [];
addItem(item: string): void {
this.itinerary.push(item);
}
showPlan(): void {
this.itinerary.forEach((item) => console.log(item));
}
}
interface VacationBuilder {
buildDay(date: string): void;
addHotel(date: string, hotelName: string): void;
addTickets(eventName: string): void;
getVacationPlanner(): VacationPlanner;
}
class PatternslandBuilder implements VacationBuilder {
private readonly planner = new VacationPlanner();
buildDay(date: string): void {
this.planner.addItem(`Day started on ${date}`);
}
addHotel(date: string, hotelName: string): void {
this.planner.addItem(`Hotel '${hotelName}' booked for ${date}`);
}
addTickets(eventName: string): void {
this.planner.addItem(`Tickets purchased for '${eventName}'`);
}
getVacationPlanner(): VacationPlanner {
return this.planner;
}
}
class Director {
constructor(private readonly builder: VacationBuilder) {}
constructPlanner(): void {
this.builder.buildDay("August 10");
this.builder.addHotel("August 10", "Grand Facadian");
this.builder.addTickets("Patterns on Ice");
}
}
const builder = new PatternslandBuilder();
new Director(builder).constructPlanner();
builder.getVacationPlanner().showPlan();
Consequences
Benefits: (GoF lists three.)
- Lets you vary a product’s internal representation. Because the product is constructed through an abstract
Builderinterface, changing its internal representation only requires defining a newConcreteBuilder. TheDirector’s construction algorithm stays the same. - Isolates code for construction and representation. Each
ConcreteBuilderencapsulates all the code to assemble one kind of product. Clients don’t need to know about the classes that make up the product’s internal structure — those classes don’t appear inBuilder’s interface. Once written, the sameConcreteBuildercan be reused by differentDirectors. - Gives you finer control over the construction process. Unlike creational patterns that build products in one shot, Builder constructs the product step by step under the director’s control. The director retrieves the product only when it is finished.
Liabilities:
- More Classes: A separate
Builderinterface and oneConcreteBuilderper representation increase the type count. - Director–Builder Coupling: A
Directorthat calls a specific sequence of builder methods is implicitly coupled to that interface.
Variant: Joshua Bloch’s Fluent Builder
The classical GoF Builder shown above uses a separate Director to drive a fixed construction algorithm. A widely-used variant — popularized by Joshua Bloch in Effective Java (Item 2) — has no Director: the client itself chains setter-style methods on the builder (new Pizza.Builder().size(12).cheese().build()) and finally calls build() to obtain the product. This fluent builder is the standard solution to the telescoping constructor anti-pattern in Java and is what most modern Java/Kotlin/C# code means by “the Builder pattern” (e.g., StringBuilder, Lombok’s @Builder, AWS SDK builders, Protocol Buffers builders). It is more about taming long parameter lists for immutable value objects than about separating construction from representation.