builder
Separate the construction of an object from its creation.
The builder design pattern.
PNG
SVG
The builder Design Pattern
Frequency
Complexity

The Builder pattern, one of the original Gang of Four design patterns, provides a strategy for creating complex objects by splitting their construction process from their actual creation. It makes it even possible to reuse this construction process to build objects of different types.

Or, in simpler words, the pattern makes the whole creation of objects maximally configurable:

It not only defines how an object is built but even what objects to built.

And as that is not enough, using the pattern you can even share the same construction process for objects that aren't in the same inheritance hierarchy!

However, despite its historical significance, it's only partially reflected in modern software. While you may still find many of its aspects today, the pattern in its complete form is not used that often anymore, as there are usually easier strategies available to solve some of the challenges it addresses.

Nevertheless, while there may not be many scenarios for this pattern, its use can sometimes still be beneficial, so it's definitely worth a closer look.

Generally, the pattern consists of two subsequent steps. Let's start with the easier one first.

The Builder Pattern, Part I:
Separating construction from creation

That sounds maybe more complicated than it actually is. We will look into this concept based on an example.

Let's say we are developing a video game - a real-time strategy game, for instance. An integral part of our game are the different levels where players have to place their units and face the enemy's army.

For this, we define a Level class that describes the structure and attributes of such levels. However, since our class can represent many different types of levels, various configuration options must be passed via the constructor.

A UML class with five attributes representing a level in a video game. 
        The constructor takes one parameter for every attribute.
A level in a video video game with many attributes. Creating such a level is cumbersome, as every attribute must be set in the constructor.

This approach has a few drawbacks:

First, as we see, our constructor is relatively complex. Callers always need to provide all parameters, even if some may not always be relevant - for instance, a mountain range level won't need any rivers. Creating different levels would require us to make constructor calls with long parameter chains, which can be hard to read. Look at the following code - It is not immediately obvious what the empty arrays in the parameter list represent.

// create plains
plainsLevel = new Level(
  new Size(20,20), Terrain.GRASS, [], [new River()], []
);

// create mountain range
mountainRangeLevel = new Level(
  new Size(20,20), Terrain.ROCKS, [], [], [new Mountain(), new Mountain()] 
);

Second, our implementation requires us to provide all parameters immediately when creating the level object. There is no way of configuring the level now and deferring its creation to later. This can be a disadvantage if, for instance, the generation of the level takes very long.

We can solve those two issues quite easily by introducing a new class LevelBuilder, which works as a type of buffer that takes all the parameters and creates the Level object only on demand when calling the getLevel() method. Additionally, our LevelBuilder could also provide default values for all parameters. Callers have to invoke setters only if they need to override those default values.

A LevelBuilder class with several methods to configure a level.
The LevelBuilder exposes several methods to define the parameters of a level. Default values are provided for all other attributes. Note that in this initial design, the getLevel() method will build and return the level object.

Using the LevelBuilder class to configure and create new levels makes our code more convenient and readable. Compare the implementation below with the constructor calls we had to invoke earlier.

// create plains
levelBuilder.setTerrain(Terrain.GRASS);
levelBuilder.addRiver();
plainLevel = levelBuilder.getLevel();

// create mountain range
levelBuilder.setTerrain(Terrain.ROCKS);
levelBuilder.addMountain();
levelBuilder.addMountain();
mountainRangeLevel = levelBuilder.getLevel();

Even in our small example with only five attributes, the code looks already easier to read and understand. Now think about what would be if the Level class had not five but ten or even more attributes. Using our LevelBuilder, we can focus only on the relevant parameters and let the Builder assign default values to all other attributes.

Great, this was already the first part of the Builder pattern - we've simplified the construction process and separated it from the actual creation of the object. Not bad, but there is still one problem: Both, construction and creation remain coupled in the same class, so we cannot reuse our construction process for other classes yet.

Let's see how we can achieve this.

The Builder Pattern, Part II:
Using the same construction process for different representations

For the second part of the pattern, we need to introduce a new class, the Director. Its purpose is to coordinate or direct the construction process of a Level. Our Director could have one method, createPlains(), that builds a level representing a terrain like the Great Plains, or a method createMountainRange() that sets up a mountain range level.

The important part is that while the Director knows how to construct a level, it should not know how to create it, so we have to remove all code used to instantiate the Level object. In our case, that's luckily only the LevelBuilder's getLevel() method.

If we do this, we have clearly separated the construction from the creation; both the Director and the LevelBuilder know how to construct a Level, but neither of them knows how to create it.

A Director class with two methods to contruct different types of levels.
The Director class coordinates the construction of specific level configurations. Note that the LevelBuilder does not have the getLevel() method anymore. Hence, while the Director can construct a level, it has no way of creating it.

Okay, fine, but how do we actually create any levels now if there is no method for it?

By subclassing our LevelBuilder - For every different representation of a Level we want to create, we define a new LevelBuilder subclass.

In the case of our game, we could, for instance, categorize Level classes based on different rendering engines. One LevelBuilder could create 2D levels while another generates 3D representations of our levels.

Two subclasses of the LevelBuilder can create either 2D or 3D levels.
Subclasses of the LevelBuilder can create different representations of levels. While the construction process stays the same, the resulting levels can be very different.

Note that while the LevelBuilders are part of a class hierarchy, the actual Levels don't need to be related. A 2D Level and a 3D Level do not necessarily require the same base class, although they're constructed by the same process.

However, as the Director operates only with the LevelBuilder base class, we must add another layer to our pattern, where we instantiate the concrete LevelBuilder subclass and inject it into the Director.

This coordination at the root level is done by the Client class. The Client acts as the entry point for our pattern by instantiating and connecting the concrete LevelBuilder and the Director. It then starts the construction process and retrieves the object finally created.

A group containing a Client and a LevelBuilder class for creating 2D Levels. There is also a similar group for creating 3D Levels. Both groups share the same Director and LevelBuilder interface.
We have one Client for creating 2D levels and another one for 3D levels. Each Client uses a dedicated LevelBuilder but shares the same construction process.

Using this approach, we can implement a game with two different representations, either in 2D or 3D. While each representation uses different level visualizations, they still share the whole creation process for each level.

And that's it: By adding the Director and the Client, we have our final Builder pattern. It allows us to encapsulate a potentially complex construction process into a separate class and lets us reuse this process for different types of Products we want to create. If we want to add new Products to our system, we only have to implement a new Builder and inject it into our Director. This makes extending our system very easy.

The final Builder pattern consisting of a Client, a Director and several Builders.
The final design of the Builder pattern: The Client injects the concrete Builder into the Director who then coordinates the construction process. After the construction is finished, the Client receives the final Product through the Concrete Builder.

The Builder Pattern Today

The Builder pattern undoubtedly introduced some interesting concepts. However, from today's perspective, it is rarely applied as a whole. But still, many of its concepts found their way into different programming languages and frameworks.

Take, for example, the Lombok Java library: It allows you to simply annotate your code with @Builder to generate an entire Builder API.

Also, most languages nowadays support optional parameters similar to the ones used in the Builder class, so there is no need to use a dedicated class to simplify object creation. The below Dart code uses optional constructor parameters, so programmers only have to set the arguments they wish to override.

class Level { 
  ...
  // define a constructor with 
  // default parameters.
  Level({ 
    this.size = Size(20,20), 
    this.terrain = Terrain.GRASS, 
    this.rivers = []
  });
}
// create Level and set parameters where necessary
Level level = Level(terrain = Terrain.ROCKS);

Regarding the pattern's ability to reuse the same construction process for different classes, I would argue this feature is rarely used today.

The reason may be that unrelated classes using the same creation process are relatively unusual. Take, for instance, the example in this article: How likely would you have a game where you could switch the engine between 2D and 3D rendering while keeping the same level structure? The only game I know where this is even possible is Dragon Quest 11, and even there, most of the 2D levels were crafted independently of their 3D counterparts.

A more typical approach today would be to generate an intermediate format with a single Builder and then transform it into the desired output. This simplifies implementation and eliminates the need for a separate Director.

  • Level images created with ChatGPT
References