Introduction
The builder pattern is a creational design pattern, i.e. it is a pattern for creating or instantiang objects of classes. It is used for breaking down the construction process, into smaller, more manageable and testable steps.
It looks like this:

Let’s break this down into its parts:
- The Director. This is the client class for the Builder, and it wants some product to be built.
- The Builder interface. This is the generic interface for any Builder, and it contains the methods to build a Product.
- The ConcreteBuilder. This is the class where we built the Product. Because we only use an interface, ConcreteBuilders are swapped in and out to build different products.
- The product we want to build is in the Product class. This could also define an interface, or be referenced through one of its superclasses.
This is all rather abstract, so we will build an example.
Implementation in Python
Importing the prerequisites
Before we start we need to import the following:
from abc import ABC, abstractmethod
from typing import List, Optional
Line by line:
- Because the base class for our builder will contain some abstract methods, it will derive from
ABC, the Abstract Base Class. Also some methods in this base class will have sub class specific implementation, so the@abstractmethoddecorator is needed. - the imports from
typingare needed to enable type annotations.Optionalis basically the equivalent ofType | Noneunion annotation.
The Bicycle class
We start by defining the basic bicycle. In our example the main properties of a bike are its type and its number of wheels:
class Bicycle:
def __init__(self):
self._bike_type: str = ""
self._number_of_wheels: int = 0
self._frame_material: str = ""
self._brake_type: str = ""
self._gear_count: int = 0
self._suspension: str = ""
self._accessories: List[str] = []
self._tire_type: str = ""
self._weight: float = 0.0
@property
def bike_type(self) -> str:
return self._bike_type
@bike_type.setter
def bike_type(self, value: str):
self._bike_type = value
@property
def number_of_wheels(self) -> int:
return self._number_of_wheels
@number_of_wheels.setter
def number_of_wheels(self, value: int):
self._number_of_wheels = value
@property
def frame_material(self) -> str:
return self._frame_material
@frame_material.setter
def frame_material(self, value: str):
self._frame_material = value
@property
def brake_type(self) -> str:
return self._brake_type
@brake_type.setter
def brake_type(self, value: str):
self._brake_type = value
@property
def gear_count(self) -> int:
return self._gear_count
@gear_count.setter
def gear_count(self, value: int):
self._gear_count = value
@property
def suspension(self) -> str:
return self._suspension
@suspension.setter
def suspension(self, value: str):
self._suspension = value
@property
def accessories(self) -> List[str]:
return self._accessories
@accessories.setter
def accessories(self, value: List[str]):
self._accessories = value
@property
def tire_type(self) -> str:
return self._tire_type
@tire_type.setter
def tire_type(self, value: str):
self._tire_type = value
@property
def weight(self) -> float:
return self._weight
@weight.setter
def weight(self, value: float):
self._weight = value
def add_accessory(self, accessory: str):
self._accessories.append(accessory)
def __str__(self):
accessories_str = ", ".join(self._accessories) if self._accessories else "None"
return (f"Bicycle Type: {self._bike_type}\n"
f"Wheels: {self._number_of_wheels}\n"
f"Frame: {self._frame_material}\n"
f"Brakes: {self._brake_type}\n"
f"Gears: {self._gear_count}\n"
f"Suspension: {self._suspension}\n"
f"Tires: {self._tire_type}\n"
f"Weight: {self._weight} kg\n"
f"Accessories: {accessories_str}")
Some points:
- A bike in our example has nine properties.
- In the constructor, we set these properties to default values.
- Next we have a series of getter and setter methods for all the properties.
- The
add_accessory()method adds to the accessories list. - The
__str__()method returns the string representation of the bicycle.
The BicycleBuilder base class
Now we come to the BicycleBuilder. This will be the base class for the builder classes, and since many of its method will be implemented by the
class BicycleBuilder(ABC):
def __init__(self):
self.reset()
def reset(self):
self._bike = Bicycle()
@abstractmethod
def set_bike_type(self) -> BicycleBuilder:
pass
@abstractmethod
def set_frame_material(self) -> BicycleBuilder:
pass
@abstractmethod
def set_wheels(self) -> BicycleBuilder:
pass
@abstractmethod
def set_brakes(self) -> BicycleBuilder:
pass
@abstractmethod
def set_gears(self) -> BicycleBuilder:
pass
@abstractmethod
def set_suspension(self) -> BicycleBuilder:
pass
@abstractmethod
def set_tires(self) -> BicycleBuilder:
pass
@abstractmethod
def set_weight(self) -> BicycleBuilder:
pass
def add_accessory(self, accessory: str) -> BicycleBuilder:
self._bike.add_accessory(accessory)
return self
def build(self) -> Bicycle:
result = self._bike
self.reset()
return result
Some notes:
- In the
BicycleBuilderclass we introduce a reset method, which resets the bicycle to be build to the default bike. - We have a bunch of methods like
set_bike_type()which are all decorated with the@abstractmethoddecorator. They will be used in the subclasses ofBicycleBuilderto set properties of the newly built bike. Note that they all return aBicycleBuilder. This means that these methods can be chained. - A bike can have accessories, however, they are not required. That is why the
add_accessoryis not an abstract method because its implementation by subclasses is not required. - Finally we have the
build()method itself, which returns the bike the builder has built, and assignsself._biketo the default bike.
Concrete implementations
We will start by implementing a builder for mountainbikes:
class MountainBikeBuilder(BicycleBuilder):
def set_bike_type(self):
self._bike.bike_type = "Mountain Bike"
return self
def set_frame_material(self):
self._bike.frame_material = "Aluminum Alloy"
return self
def set_wheels(self):
self._bike.number_of_wheels = 2
return self
def set_brakes(self):
self._bike.brake_type = "Hydraulic Disc"
return self
def set_gears(self):
self._bike.gear_count = 21
return self
def set_suspension(self):
self._bike.suspension = "Full Suspension"
return self
def set_tires(self):
self._bike.tire_type = "Knobby Off-Road"
return self
def set_weight(self):
self._bike.weight = 14.5
return self
class RoadBikeBuilder(BicycleBuilder):
def set_bike_type(self):
self._bike.bike_type = "Road Bike"
return self
def set_frame_material(self):
self._bike.frame_material = "Carbon Fiber"
return self
def set_wheels(self):
self._bike.number_of_wheels = 2
return self
def set_brakes(self):
self._bike.brake_type = "Caliper"
return self
def set_gears(self):
self._bike.gear_count = 16
return self
def set_suspension(self):
self._bike.suspension = "Rigid"
return self
def set_tires(self):
self._bike.tire_type = "Smooth Road"
return self
def set_weight(self):
self._bike.weight = 8.2
return self
Observe that we do not need to implement the build() or reset() methods here, since they are part of the base class.
Next, the builder for an electric bike:
class ElectricBikeBuilder(BicycleBuilder):
def set_bike_type(self):
self._bike.bike_type = "Electric Bike"
return self
def set_frame_material(self):
self._bike.frame_material = "Steel"
return self
def set_wheels(self):
self._bike.number_of_wheels = 2
return self
def set_brakes(self):
self._bike.brake_type = "Mechanical Disc"
return self
def set_gears(self):
self._bike.gear_count = 7
return self
def set_suspension(self):
self._bike.suspension = "Front Fork"
return self
def set_tires(self):
self._bike.tire_type = "Hybrid"
return self
def set_weight(self):
self._bike.weight = 22.0
return self
And we will also implement a RoadBikeBuilder:
class RoadBikeBuilder(BicycleBuilder):
def set_bike_type(self):
self._bike.bike_type = "Road Bike"
return self
def set_frame_material(self):
self._bike.frame_material = "Carbon Fiber"
return self
def set_wheels(self):
self._bike.number_of_wheels = 2
return self
def set_brakes(self):
self._bike.brake_type = "Caliper"
return self
def set_gears(self):
self._bike.gear_count = 16
return self
def set_suspension(self):
self._bike.suspension = "Rigid"
return self
def set_tires(self):
self._bike.tire_type = "Smooth Road"
return self
def set_weight(self):
self._bike.weight = 8.2
return self
Implementing the BicycleDirector
Now that we have all the methods to set features on the bicycles, we need a way to put them together. That is the task of the BicycleDirector:
class BicycleDirector:
def __init__(self, builder: BicycleBuilder):
self._builder = builder
def construct_basic_bicycle(self) -> Bicycle:
return (self._builder
.set_bike_type()
.set_frame_material()
.set_wheels()
.set_brakes()
.set_gears()
.build())
def construct_full_bicycle(self) -> Bicycle:
return (self._builder
.set_bike_type()
.set_frame_material()
.set_wheels()
.set_brakes()
.set_gears()
.set_suspension()
.set_tires()
.set_weight()
.build())
def construct_custom_bicycle(self, accessories: Optional[List[str]] = None) -> Bicycle:
if accessories is None:
accessories = []
bicycle = (self._builder
.set_bike_type()
.set_frame_material()
.set_wheels()
.set_brakes()
.set_gears()
.set_suspension()
.set_tires()
.set_weight())
for accessory in accessories:
bicycle.add_accessory(accessory)
return bicycle.build()
Again some notes:
- We have three methods. The first one is to build a basic bicycle which for example lacks suspension. The second method is to build a full bicycle, with all the features, and the third one is to build a custom bicycle where some accessories can be added.
- The constructor receives a
BicycleBuilderas an argument. Note that since for exampleMountainBikeBuilderinherits fromBicycleBuilderit can be passed to the constructor. This allows us to use theBicycleDirectorclass with any builder class deriving fromBicycleBuilder.
Time to test
Now we can test our code:
if __name__ == "__main__":
print("=== Builder Pattern Demo ===\n")
mountain_builder = MountainBikeBuilder()
director = BicycleDirector(mountain_builder)
print("1. Full Mountain Bike:")
mountain_bike = director.construct_full_bicycle()
print(mountain_bike)
print()
road_builder = RoadBikeBuilder()
director = BicycleDirector(road_builder)
print("2. Custom Road Bike with accessories:")
road_bike = director.construct_custom_bicycle(["Water Bottle", "Bike Computer", "LED Lights"])
print(road_bike)
print()
electric_builder = ElectricBikeBuilder()
print("3. Electric Bike built directly:")
electric_bike = (electric_builder
.set_bike_type()
.set_frame_material()
.set_wheels()
.set_brakes()
.set_gears()
.add_accessory("Battery")
.add_accessory("Display")
.build())
print(electric_bike)
print()
print("4. Reusing Mountain Bike builder for basic bike:")
basic_mountain = director.construct_basic_bicycle()
print(basic_mountain)
Line by line:
- We start by constructing a
MountainBikeBuilderand passing it to the constructor of theBicycleDirectorclass. - Now we can construct a full mountain bike and print it out.
- Next we do the same for a road bike but this time we add some accessories.
- Next to demonstrate that we can also construct a bike by hand by doing that for an electric bike.
- Next we show that we can also build a basic road bike from the same director.
Conclusion
The Builder pattern is a powerful creational design pattern that allows for the step-by-step construction of complex objects. As we’ve seen through the bicycle example, it provides a clear separation between the object’s construction and its representation. This makes the code more readable, maintainable, and flexible.
By using a Director class, we can encapsulate the construction process and reuse it with different Builder implementations to create various product configurations. This pattern is particularly useful when an object can have many different attributes, some of which are optional. It prevents the need for a “telescoping constructor” with numerous parameters, which can be hard to manage and understand. Ultimately, the Builder pattern enhances code flexibility and simplifies the creation of diverse and complex objects.




