Skip to content

Builder Pattern in Java – A Delicious Pizza Example

By Ravi Shankar · officialcto.com


Introduction

Some objects are simple to construct — you pass two or three arguments, and you’re done.

But what happens when an object has many optional fields?

Consider building a Pizza:

  • Base: thin crust, regular, deep-dish
  • Sauce: tomato, pesto, barbecue
  • Topping: cheese, chicken, veggies, or combinations

If we try to handle this with constructors, we quickly fall into the telescoping constructor anti-pattern.


The Telescoping Constructor Problem

java
Pizza pizza = new Pizza("thin crust", "pesto", "chicken");

Looks fine. But what if tomorrow we add:

  • Extra cheese (boolean)
  • Size (enum: SMALL, MEDIUM, LARGE)
  • Gluten-free (boolean)

Now we need multiple overloaded constructors — messy, hard to read, and error-prone.


The Builder Pattern

Intent:
The Builder pattern provides a step-by-step way to construct complex objects.

  • Keeps the object immutable (fields are final).
  • Provides a fluent API (.sauce("pesto").topping("chicken")).
  • Handles defaults for optional fields.
  • Allows validation before building.

UML Sketch

       +----------------+
       |    Pizza       |  (Product)
       |----------------|
       | - base         |
       | - sauce        |
       | - topping      |
       +----------------+
               ^
               |
        builds from
               |
       +----------------+
       | Pizza.Builder  |  (Builder)
       |----------------|
       | + base(...)    |
       | + sauce(...)   |
       | + topping(...) |
       | + build()      |
       +----------------+

The Pizza Class with Builder

java
public class Pizza {
  private final String base;
  private final String sauce;
  private final String topping;

  private Pizza(String base, String sauce, String topping) {
    this.base = base;
    this.sauce = sauce;
    this.topping = topping;
  }

  public String getBase() { return base; }
  public String getSauce() { return sauce; }
  public String getTopping() { return topping; }

  public static class Builder {
    private String base = "regular";
    private String sauce = "tomato";
    private String topping = "cheese";

    public Builder base(String base) {
      this.base = base;
      return this;
    }

    public Builder sauce(String sauce) {
      this.sauce = sauce;
      return this;
    }

    public Builder topping(String topping) {
      this.topping = topping;
      return this;
    }

    public Pizza build() {
      if (base == null || base.isBlank()) {
        throw new IllegalStateException("Pizza base is required");
      }
      return new Pizza(base, sauce, topping);
    }
  }
}

Usage Example

java
public class Main {
  public static void main(String[] args) {
    Pizza pizza = new Pizza.Builder()
        .base("thin crust")
        .sauce("pesto")
        .topping("chicken")
        .build();

    System.out.println("Pizza with " 
        + pizza.getBase() + " base, " 
        + pizza.getSauce() + " sauce, and " 
        + pizza.getTopping() + " topping created.");
  }
}

Output

Pizza with thin crust base, pesto sauce, and chicken topping created.

Why is this the Builder Pattern?

  1. Readable: Method chaining makes the construction expressive.
  2. Immutable: Once built, Pizza fields cannot be changed.
  3. Defaults: If you don’t specify sauce, it defaults to tomato.
  4. Validation: Builder can enforce rules (e.g., base cannot be empty).
  5. Extensible: Adding new options (size, extra cheese) is easy — just add more builder methods.

Advantages

  • Cleaner construction of complex objects.
  • No explosion of constructors.
  • Easy to add optional fields.
  • Supports immutability.
  • Improves readability with fluent API.

Limitations

  • More boilerplate code compared to simple constructors.
  • For trivial objects, Builder may be overkill.
  • Without tools like Lombok, you must manually maintain the Builder.

TODOs and Suggested Improvements

  1. Add more fields: Extend Pizza with size, extraCheese, glutenFree.
  2. Required fields: Enforce mandatory options (e.g., base) via the Builder’s constructor.
  3. Validation logic: Prevent invalid pizzas (e.g., gluten-free + deep-dish not allowed).
  4. Factory integration: Combine with Abstract Factory — ItalianPizzaFactory could return pre-seeded builders with defaults.
  5. Testing: Add JUnit tests to verify default values and validation rules.
  6. Lombok: Use @Builder annotation to generate the builder automatically and compare.

Conclusion

The Builder pattern is an elegant solution when object construction gets messy.
In our example, instead of juggling overloaded constructors, we used a fluent, immutable builder for Pizza.

This pattern is widely applicable — from configuring pizzas to building HTTP requests or complex UI components.

Once you get comfortable with Builder, you’ll start noticing where it can simplify code and improve readability.


✍️ Written by Ravi Shankar for officialcto.com