Builder Design Pattern - Basic Implementation
From Constructor to Basic Builder
Introduction
This blog entry walks you through how to apply the Builder Pattern in Java—from constructors with long parameter lists, to fluent setters, to a complete basic Builder implementation. Along the way, we’ll discuss design tradeoffs, give you practical code, and explain why the Builder Pattern is still relevant.
You’ve Tried the Rest, Now Try the Best
The bulk of this blog post will feature Java code demonstrating a complex object being declared via a traditional constructor with implementation updates until it’s declared via a Builder.
The complex object will be a Pizza
class for which instances declare the pizza’s size with optional toppings.
I’m only concerned with declaring a Pizza
instance in this blog. There won’t be any behavior related methods except for a toString()
method, which formats and returns a String descripiton of the Pizza
instance.
Constructor Implementation
My initial design declares Pizza
with a constructor that declares the size and several toppings as constructor arguments:
import java.util.*;
public class PizzaBuilder1 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza(Pizza.Size.LARGE, true, false, true, false);
System.out.println(pizza1);
Pizza pizza2 = new Pizza(Pizza.Size.MEDIUM, true, true, true, true);
System.out.println(pizza2);
Pizza pizza3 = new Pizza(Pizza.Size.SMALL, false, false, false, false);
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private final boolean pepperoni;
private final boolean peppers;
private final boolean onions;
private final boolean blackOlives;
public Pizza(Size size, boolean pepperoni, boolean peppers, boolean onions, boolean blackOlives) {
this.size = size;
this.pepperoni = pepperoni;
this.peppers = peppers;
this.onions = onions;
this.blackOlives = blackOlives;
}
@Override
public String toString() {
StringBuilder description = new StringBuilder();
description.append("Pizza Size: ").append(size);
description.append(" with toppings:");
boolean hasToppings = false;
if (pepperoni) {
description.append(" Pepperoni");
hasToppings = true;
}
if (peppers) {
if (hasToppings) description.append(",");
description.append(" Peppers");
hasToppings = true;
}
if (onions) {
if (hasToppings) description.append(",");
description.append(" Onions");
hasToppings = true;
}
if (blackOlives) {
if (hasToppings) description.append(",");
description.append(" Black Olives");
hasToppings = true;
}
if (!hasToppings) {
description.append(" None");
}
return description.toString();
}
}
Pizza
is a Value Object, which I’ll blog about in the future (TBD). Its attributes are final
, meaning that a Pizza
object is immutable, which is a desirable trait, but there are few parts of this basic design that I don’t like:
- The constructor defines the
Size
parameter along with fourboolean
parameters. A large number of arguments, especially repeating basic types, such as booleans, can be confusing. It’s not obvious which boolean argument value aligns with which parameter. Most IDEs will add the field name, which provides some context, but that also violates encapsulation to some degree. - These are not the only topping options. More will be added, such as sausage, mushrooms and pineapple … okay, never pineapple. I limited this example to four topping parameters just so it would not become too massive as an example, but I can easily envision a dozen or more topping options. This would make the constructor parameter list even more confusing. And if we added another topping, it could force modifications in every existing Pizza constructor reference.
NOTE: The only issue I have with this Pizza
implementation is the number of parameters defined in the constructor, especially since the number of parameters could continue to accumulate. If Pizza
had a smaller stable constructor parameter list with types providing context, then I would have no issues with that implementation. Most classes tend to have stable context oriented constructor parameter lists.
Constructor to Set Accessors
There’s more than one way to inject field attribute values into an object. The constructor is one option, and set accessors are another.
Here’s an implementation that reduces the constructor to one argument for the Pizza
Size
and allows the client code to add toppings as needed via set accessor methods:
import java.util.*;
public class PizzaBuilder2 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza(Pizza.Size.LARGE);
pizza1.addPepperoni();
pizza1.addOnions();
System.out.println(pizza1);
Pizza pizza2 = new Pizza(Pizza.Size.MEDIUM);
pizza2.addPepperoni();
pizza2.addPeppers();
pizza2.addOnions();
pizza2.addBlackOlives();
System.out.println(pizza2);
Pizza pizza3 = new Pizza(Pizza.Size.SMALL);
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private boolean pepperoni = false;
private boolean peppers = false;
private boolean onions = false;
private boolean blackOlives = false;
public Pizza(Size size) {
this.size = size;
}
public void addPepperoni() {
pepperoni = true;
}
public void addPeppers() {
peppers = true;
}
public void addOnions() {
onions = true;
}
public void addBlackOlives() {
blackOlives = true;
}
@Override
public String toString() {
// Same toString() implementation as above. It will not change as the design progresses.
}
}
This solves the long constructor argument problem, and it provides more context, since the accessors have meaningful names. But it’s broken the class initialization into multiple statements, and we’ve lost the immutable nature of the previous implementation. A new topping can be added at any time, even after the order were filled, which probably isn’t desirable.
When using a mechanism like this, we have to ensure the default behavior for any element not set still makes sense. It’s not an issue for Pizza
toppings, since the absense of listing a topping just means that the topping is not desired for the pizza. For example, a Pizza
without toppings is a plain cheese pizza, which is a perfectly valid choice.
Chaining Accessors
This update modifies the accessors slightly so that they return this
rather than void
. This allows us to chain the Pizza
construction making it feel more like a unit:
import java.util.*;
public class PizzaBuilder3 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza(Pizza.Size.LARGE)
.addPepperoni()
.addOnions();
System.out.println(pizza1);
Pizza pizza2 = new Pizza(Pizza.Size.MEDIUM)
.addPepperoni()
.addPeppers()
.addOnions()
.addBlackOlives();
System.out.println(pizza2);
Pizza pizza3 = new Pizza(Pizza.Size.SMALL);
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private boolean pepperoni = false;
private boolean peppers = false;
private boolean onions = false;
private boolean blackOlives = false;
public Pizza(Size size) {
this.size = size;
}
public Pizza addPepperoni() {
pepperoni = true;
return this;
}
public Pizza addPeppers() {
peppers = true;
return this;
}
public Pizza addOnions() {
onions = true;
return this;
}
public Pizza addBlackOlives() {
blackOlives = true;
return this;
}
@Override
public String toString() {
// Same toString() implementation original.
}
}
This is a bit better, but it’s just syntactic sugar. The Pizza
object is still mutable and a new topping can be added at any time, including after the order has been filled.
Chaining is an implementation style that often emerges when violating the Law of Demeter. Chaining pulls more classes and their dependencies into the code that uses it. However, this type of Pizza chaining is not a violation of the Law of Demter, since each link the chain still references the Pizza
instance. No new classes or dependencies have been introduced.
The Pizza.Builder
This implementation is the union of the first and third implementations, where Pizza
is immutable, and Pizza.Builder
allows non-Demeter violating chaining. For a more detailed description of this technique, see Exploring Joshua Bloch’s Builder design pattern in Java.
import java.util.*;
public class PizzaBuilder4 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza.Builder(Pizza.Size.LARGE)
.addPepperoni()
.addOnions()
.build();
System.out.println(pizza1);
Pizza pizza2 = new Pizza.Builder(Pizza.Size.MEDIUM)
.addPepperoni()
.addPeppers()
.addOnions()
.addBlackOlives()
.build();
System.out.println(pizza2);
Pizza pizza3 = new Pizza.Builder(Pizza.Size.SMALL)
.build();
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private final boolean pepperoni;
private final boolean peppers;
private final boolean onions;
private final boolean blackOlives;
private Pizza(Builder builder) {
this.size = builder.size;
this.pepperoni = builder.pepperoni;
this.peppers = builder.peppers;
this.onions = builder.onions;
this.blackOlives = builder.blackOlives;
}
public static class Builder {
private Size size;
private boolean pepperoni = false;
private boolean peppers = false;
private boolean onions = false;
private boolean blackOlives = false;
public Builder(Size size) {
this.size = size;
}
public Builder addPepperoni() {
pepperoni = true;
return this;
}
public Builder addPeppers() {
peppers = true;
return this;
}
public Builder addOnions() {
onions = true;
return this;
}
public Builder addBlackOlives() {
blackOlives = true;
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
@Override
public String toString() {
// Same toString() implementation original.
}
}
This implementation includes several of the attributes of the Builder pattern presented in the Builder Design Pattern Introduction blog entry:
- The code in
main()
is the Client. PizzaBuilder
is the ConcreteBuilder.Pizza
is the Product.
There are several Builder attributes that are not included in this example; although, this is also the case for the Real World Examples from the previous blog:
- There is no abstract Builder; therefore, there’s no option for multiple ConcreteBuilders.
- There is no Director supervising the construction. The construction is hardcoded in
main()
. - There is no specification script to build different pizzas. Every
Pizza
instance is hardcoded.
I will be adding these missing attributes in subsequent blog entries.
There’s a bit more code and complexity to this implementation than in the earlier implementation examples, so each developer or team will need to decide if it’s worth the investment. However, this PizzaDriver
implementation ensures that Pizza
can only be constructed via the Pizza.Builder
and once built, the Pizza
instance is immutable.
Summary
The Builder pattern isn’t just a way to reduce constructor clutter—it’s a tool that brings clarity and control to object creation. Try applying it in your next project when you need flexible, readable instantiation of complex objects. And stay tuned—we’ll look at how to structure reusable builders and abstract the process even further in the next article.
References
See: Previous Blog References, which provides an extensive list of Builder resources.
Complete Demo Code
Here’s the entire implementation up to this point as one file. Copy and paste it into a Java environment and execute it. If you don’t have Java, try this Online Java Environment. Add more tests. Play with the implementation. Refactor some of the code.
Constructor
import java.util.*;
public class PizzaBuilder1 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza(Pizza.Size.LARGE, true, false, true, false);
System.out.println(pizza1);
Pizza pizza2 = new Pizza(Pizza.Size.MEDIUM, true, true, true, true);
System.out.println(pizza2);
Pizza pizza3 = new Pizza(Pizza.Size.SMALL, false, false, false, false);
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private final boolean pepperoni;
private final boolean peppers;
private final boolean onions;
private final boolean blackOlives;
public Pizza(Size size, boolean pepperoni, boolean peppers, boolean onions, boolean blackOlives) {
this.size = size;
this.pepperoni = pepperoni;
this.peppers = peppers;
this.onions = onions;
this.blackOlives = blackOlives;
}
@Override
public String toString() {
StringBuilder description = new StringBuilder();
description.append("Pizza Size: ").append(size);
description.append(" with toppings:");
boolean hasToppings = false;
if (pepperoni) {
description.append(" Pepperoni");
hasToppings = true;
}
if (peppers) {
if (hasToppings) description.append(",");
description.append(" Peppers");
hasToppings = true;
}
if (onions) {
if (hasToppings) description.append(",");
description.append(" Onions");
hasToppings = true;
}
if (blackOlives) {
if (hasToppings) description.append(",");
description.append(" Black Olives");
hasToppings = true;
}
if (!hasToppings) {
description.append(" None");
}
return description.toString();
}
}
Set Accessors
import java.util.*;
public class PizzaBuilder2 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza(Pizza.Size.LARGE);
pizza1.addPepperoni();
pizza1.addOnions();
System.out.println(pizza1);
Pizza pizza2 = new Pizza(Pizza.Size.MEDIUM);
pizza2.addPepperoni();
pizza2.addPeppers();
pizza2.addOnions();
pizza2.addBlackOlives();
System.out.println(pizza2);
Pizza pizza3 = new Pizza(Pizza.Size.SMALL);
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private boolean pepperoni = false;
private boolean peppers = false;
private boolean onions = false;
private boolean blackOlives = false;
public Pizza(Size size) {
this.size = size;
}
public void addPepperoni() {
pepperoni = true;
}
public void addPeppers() {
peppers = true;
}
public void addOnions() {
onions = true;
}
public void addBlackOlives() {
blackOlives = true;
}
@Override
public String toString() {
StringBuilder description = new StringBuilder();
description.append("Pizza Size: ").append(size);
description.append(" with toppings:");
boolean hasToppings = false;
if (pepperoni) {
description.append(" Pepperoni");
hasToppings = true;
}
if (peppers) {
if (hasToppings) description.append(",");
description.append(" Peppers");
hasToppings = true;
}
if (onions) {
if (hasToppings) description.append(",");
description.append(" Onions");
hasToppings = true;
}
if (blackOlives) {
if (hasToppings) description.append(",");
description.append(" Black Olives");
hasToppings = true;
}
if (!hasToppings) {
description.append(" None");
}
return description.toString();
}
}
Chaining
import java.util.*;
public class PizzaBuilder3 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza(Pizza.Size.LARGE)
.addPepperoni()
.addOnions();
System.out.println(pizza1);
Pizza pizza2 = new Pizza(Pizza.Size.MEDIUM)
.addPepperoni()
.addPeppers()
.addOnions()
.addBlackOlives();
System.out.println(pizza2);
Pizza pizza3 = new Pizza(Pizza.Size.SMALL);
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private boolean pepperoni = false;
private boolean peppers = false;
private boolean onions = false;
private boolean blackOlives = false;
public Pizza(Size size) {
this.size = size;
}
public Pizza addPepperoni() {
pepperoni = true;
return this;
}
public Pizza addPeppers() {
peppers = true;
return this;
}
public Pizza addOnions() {
onions = true;
return this;
}
public Pizza addBlackOlives() {
blackOlives = true;
return this;
}
@Override
public String toString() {
StringBuilder description = new StringBuilder();
description.append("Pizza Size: ").append(size);
description.append(" with toppings:");
boolean hasToppings = false;
if (pepperoni) {
description.append(" Pepperoni");
hasToppings = true;
}
if (peppers) {
if (hasToppings) description.append(",");
description.append(" Peppers");
hasToppings = true;
}
if (onions) {
if (hasToppings) description.append(",");
description.append(" Onions");
hasToppings = true;
}
if (blackOlives) {
if (hasToppings) description.append(",");
description.append(" Black Olives");
hasToppings = true;
}
if (!hasToppings) {
description.append(" None");
}
return description.toString();
}
}
Builder
import java.util.*;
public class PizzaBuilder4 {
public static void main(String[] args) {
Pizza pizza1 = new Pizza.Builder(Pizza.Size.LARGE)
.addPepperoni()
.addOnions()
.build();
System.out.println(pizza1);
Pizza pizza2 = new Pizza.Builder(Pizza.Size.MEDIUM)
.addPepperoni()
.addPeppers()
.addOnions()
.addBlackOlives()
.build();
System.out.println(pizza2);
Pizza pizza3 = new Pizza.Builder(Pizza.Size.SMALL)
.build();
System.out.println(pizza3);
}
}
public class Pizza {
public enum Size {
SMALL, MEDIUM, LARGE
}
private final Size size;
private final boolean pepperoni;
private final boolean peppers;
private final boolean onions;
private final boolean blackOlives;
private Pizza(Builder builder) {
this.size = builder.size;
this.pepperoni = builder.pepperoni;
this.peppers = builder.peppers;
this.onions = builder.onions;
this.blackOlives = builder.blackOlives;
}
public static class Builder {
private Size size;
private boolean pepperoni = false;
private boolean peppers = false;
private boolean onions = false;
private boolean blackOlives = false;
public Builder(Size size) {
this.size = size;
}
public Builder addPepperoni() {
pepperoni = true;
return this;
}
public Builder addPeppers() {
peppers = true;
return this;
}
public Builder addOnions() {
onions = true;
return this;
}
public Builder addBlackOlives() {
blackOlives = true;
return this;
}
public Pizza build() {
return new Pizza(this);
}
}
@Override
public String toString() {
StringBuilder description = new StringBuilder();
description.append("Pizza Size: ").append(size);
description.append(" with toppings:");
boolean hasToppings = false;
if (pepperoni) {
description.append(" Pepperoni");
hasToppings = true;
}
if (peppers) {
if (hasToppings) description.append(",");
description.append(" Peppers");
hasToppings = true;
}
if (onions) {
if (hasToppings) description.append(",");
description.append(" Onions");
hasToppings = true;
}
if (blackOlives) {
if (hasToppings) description.append(",");
description.append(" Black Olives");
hasToppings = true;
}
if (!hasToppings) {
description.append(" None");
}
return description.toString();
}
}