The Builder Pattern in Java: A Tasty Example


The Builder pattern is a creational design pattern used to construct complex objects step-by-step. It’s useful when a class has many optional parameters or an object construction needs to be made more readable and flexible.
In this post, we’ll explore the Builder pattern through a fun and relatable example: building a custom sandwich.
1. What Is the Builder Pattern and When to Use It?
The Builder Pattern lets you separate object construction from its representation. It’s helpful when:
You have a class with many parameters
Some parameters are optional
You want to make object creation readable and safe
You want to avoid telescoping constructors (constructors with many overloads)
🍞 The Basic Recipe for the Builder Pattern
Before jumping into the full example, here’s a quick overview of how the Builder pattern is typically implemented:
Prerequisite: You have a class with many fields or complex configuration logic.
🔧 Steps:
In the enclosing class (the one with many fields), define a static nested
Builder
class.In the
Builder
class, declare the same fields as in the enclosing class.In the
Builder
constructor, set any required fields.In the
Builder
, provide setter methods (e.g.,setX(value)
) for optional fields.Ensure each setter returns
this
(the builder instance) so you can chain calls fluently.In the
Builder
, implement abuild()
method that returns the final object.In the enclosing class, create a private constructor that initializes its fields using the builder’s fields.
2. A Sandwich Builder Example
Let’s say you want to create a customizable sandwich with required and optional ingredients.
package design_patterns;
import java.util.List;
public class Sandwich {
//required fields
private final String breadType;
private final String mainFilling;
//optional fields
private final String cheese;
private final List<String> veggies;
private final List<String> sauces;
private final boolean toasted;
private Sandwich(Builder builder) {
this.breadType = builder.breadType;
this.mainFilling = builder.mainFilling;
this.cheese = builder.cheese;
this.veggies = builder.veggies;
this.sauces = builder.sauces;
this.toasted = builder.toasted;
}
public static Builder builder(String breadType, String mainFilling) {
return new Builder(breadType, mainFilling);
}
@Override
public String toString() {
return String.format(
"Sandwich: \n- Bread: %s\n- Main Filling: %s\n- Cheese: %s\n- Veggies: %s\n- Sauces: %s\n- Toasted: %s",
breadType,
mainFilling,
cheese != null ? cheese : "None",
veggies != null ? veggies : "None",
sauces != null ? sauces : "None",
toasted ? "Yes" : "No"
);
}
public static class Builder {
private final String breadType;
private final String mainFilling;
private String cheese;
private List<String> veggies;
private List<String> sauces;
private boolean toasted;
public Builder(String breadType, String mainFilling) {
this.breadType = breadType;
this.mainFilling = mainFilling;
this.veggies = List.of();
this.sauces = List.of();
}
public Builder setCheese(String cheese) {
this.cheese = cheese;
return this;
}
public Builder setVeggies(List<String> veggies) {
this.veggies = veggies.stream().toList();
return this;
}
public Builder setSauces(List<String> sauces) {
this.sauces = sauces.stream().toList();
return this;
}
public Builder setToasted(boolean toasted) {
this.toasted = toasted;
return this;
}
public Sandwich build() {
return new Sandwich(this);
}
}
}
And here’s how you would use it:
package design_patterns;
import java.util.List;
public class BuilderPatternExample {
public static void main(String[] args) {
Sandwich mySandwich = Sandwich.builder("Whole Wheat", "Chicken")
.setCheese("Cheddar")
.setToasted(true)
.setSauces(List.of("Honey Mustard"))
.setVeggies(List.of("Green Pepper", "Red Pepper", "Lettuce"))
.build();
System.out.println(mySandwich);
}
}
3. Essentials That Make the Builder Pattern Work
Private constructor: The
Sandwich
constructor is private so clients are forced to use the builder.Immutable fields: All fields in
Sandwich
arefinal
, making the object immutable after construction.Required vs Optional Fields: Required fields are set in the builder constructor, while optional fields have setters.
Static Builder class: Declaring
Builder
as a static nested class avoids needing a reference to the outerSandwich
.Fluent API: Each setter returns
this
to support method chaining.Extensibility: Easy to add more optional ingredients without changing the constructor signatures.
4. Pro-Level Improvements for a Great Builder API
Static
builder()
method: Makes usage cleaner thannew Sandwich.Builder(...)
.Defensive copying: Lists are copied using
stream().toList()
to prevent external mutation.Default values: Optional fields like
veggies
andsauces
default to empty lists.Readable
toString()
: CustomtoString()
improves debuggability and demo value.
5. Real-World Examples of the Builder Pattern in Frameworks
The Builder pattern is not just a design exercise — it’s widely used in real frameworks and libraries to improve code readability and reduce constructor complexity.
🔧 Lombok (@Builder
)
In Java, Project Lombok offers a @Builder
annotation that auto-generates builder code for you. It’s great for data classes where you want clean, fluent APIs without boilerplate.
@Builder
public class User {
private String name;
private int age;
}
Usage:
User user = User.builder()
.name("Alice")
.age(30)
.build();
🌱 Spring Framework
While Spring is best known for dependency injection, many Spring projects (like Spring Security, Spring Boot, and Spring Data) use builder-style APIs to configure components:
HttpSecurity http = ...;
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
This fluent configuration style is inspired by the Builder pattern, providing clarity and flexibility when building complex object graphs.
☁️ AWS SDK (Java)
The AWS SDK uses the Builder pattern extensively to construct requests:
PutObjectRequest request = PutObjectRequest.builder()
.bucket("my-bucket")
.key("my-file.txt")
.contentType("text/plain")
.build();
This avoids long constructors and makes API usage expressive and maintainable.
6. Trade-Offs of the Builder Pattern
While the Builder pattern improves clarity and flexibility, it comes with a few trade-offs:
Extra boilerplate: Writing a builder adds more code compared to using constructors or setters directly.
Adds minor indirection: The object creation is split between the builder and the target class
Not ideal for simple objects: For classes with only a few fields, the pattern may feel unnecessary.
Despite these, the Builder pattern remains a clean and scalable solution for constructing complex or immutable objects.
Conclusion
The Builder pattern brings fluency, clarity, and structure to object creation. It’s widely adopted in frameworks and libraries to improve the readability and flexibility of configuration.
This is one of those patterns that feels sweet and mellow — the trade-offs are minor compared to the flexibility you gain, especially when working with complex objects.
I hope that the next time you enjoy a good sandwich, you’ll think of the Builder pattern. Enjoy! 🥪!
Subscribe to my newsletter
Read articles from Mirna De Jesus Cambero directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mirna De Jesus Cambero
Mirna De Jesus Cambero
I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.