Builder Pattern in Java: Clear Examples and Real Application
Source: Dev.to
Builder Design Pattern in Java
The Builder design pattern is a creational pattern that separates the construction of an object from its representation. In other words, it enables the creation of an object step‑by‑step, which is especially useful for complex objects.
When should you use the Builder pattern?
Imagine an object with 8 attributes. Writing a constructor with 8 arguments works, but what if you don’t need all of them? Passing null for unused parameters is a bad practice, and creating a constructor for every possible combination quickly becomes unmanageable.
Solution: Apply the Builder pattern to provide a readable, flexible way of constructing such objects.
Advantages of using the Builder pattern
| Advantage | Description |
|---|---|
| Flexibility | Create objects with any subset of parameters in a natural, fluent way. |
| Readability | The construction code is easy to follow and understand. |
| Immutability | Once built, the object can be immutable, guaranteeing thread‑safety against unwanted modifications. |
How is the Builder pattern implemented in Java?
Design patterns are abstract solutions, so implementations can be adapted to the context. Below are several concrete examples.
1. Classic Builder Design Pattern in Java
The classic approach defines a static inner Builder class that mirrors the fields of the product class. The product’s fields are private final (immutable) and have only getters.
package com.funcionaenmimaquina.builder.classic;
public class Product {
private final String name;
private final String description;
private final double price;
private final String category;
private final String imageUrl;
private final String brand;
private final String sku;
private Product(Builder builder) {
this.name = builder.name;
this.description = builder.description;
this.price = builder.price;
this.category = builder.category;
this.imageUrl = builder.imageUrl;
this.brand = builder.brand;
this.sku = builder.sku;
}
/** Builder ----------------------------------------------------------- */
public static class Builder {
private String name;
private String description;
private double price;
private String category;
private String imageUrl;
private String brand;
private String sku;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder description(String description) {
this.description = description;
return this;
}
public Builder price(double price) {
this.price = price;
return this;
}
public Builder category(String category) {
this.category = category;
return this;
}
public Builder imageUrl(String imageUrl) {
this.imageUrl = imageUrl;
return this;
}
public Builder brand(String brand) {
this.brand = brand;
return this;
}
public Builder sku(String sku) {
this.sku = sku;
return this;
}
public Product build() {
return new Product(this);
}
}
/** Getters ----------------------------------------------------------- */
public String getName() { return name; }
public String getDescription() { return description; }
public double getPrice() { return price; }
public String getCategory() { return category; }
public String getImageUrl() { return imageUrl; }
public String getBrand() { return brand; }
public String getSku() { return sku; }
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", description='" + description + '\'' +
", price=" + price +
", category='" + category + '\'' +
", imageUrl='" + imageUrl + '\'' +
", brand='" + brand + '\'' +
", sku='" + sku + '\'' +
'}';
}
}
Using the classic builder
static void runClassicBuilder() {
System.out.println("Classic Builder Pattern Example");
Product product = new Product.Builder()
.name("Laptop")
.description("High performance laptop")
.price(1200.00)
.category("Electronics")
.imageUrl("http://example.com/laptop.jpg")
.brand("BrandX")
.sku("SKU12345")
.build();
System.out.println(product);
}
2. Generic Builder Design Pattern with Lambdas
Lambdas let us create a generic builder that can be reused for any class.
import java.util.function.BiConsumer;
import java.util.function.Supplier;
public class Builder<T> {
private final Supplier<T> supplier;
private final T instance;
private Builder(Supplier<T> supplier) {
this.supplier = supplier;
this.instance = supplier.get();
}
/** Factory method */
public static <T> Builder<T> of(Supplier<T> supplier) {
return new Builder<>(supplier);
}
/** Apply a setter (or any mutator) to the instance */
public <P> Builder<T> with(BiConsumer<T, P> setter, P value) {
setter.accept(instance, value);
return this;
}
/** Return the fully built object */
public T build() {
return instance;
}
}
Example usage with a simple POJO
public class Person {
private String firstName;
private String lastName;
private int age;
// Setters (or any mutators) – they can be package‑private if you want
public void setFirstName(String firstName) { this.firstName = firstName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public void setAge(int age) { this.age = age; }
@Override
public String toString() {
return firstName + " " + lastName + ", age " + age;
}
}
static void runGenericBuilder() {
Person person = Builder.of(Person::new)
.with(Person::setFirstName, "John")
.with(Person::setLastName, "Doe")
.with(Person::setAge, 30)
.build();
System.out.println(person);
}
Example with a User class
public class User {
private String name;
private String lastname;
private String secondLastName;
private String phone;
private String email;
// Getters and Setters needed
public void setName(String name) { this.name = name; }
public void setLastname(String lastname) { this.lastname = lastname; }
public void setSecondLastName(String secondLastName) { this.secondLastName = secondLastName; }
public void setPhone(String phone) { this.phone = phone; }
public void setEmail(String email) { this.email = email; }
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", lastname='" + lastname + '\'' +
", secondLastName='" + secondLastName + '\'' +
", phone='" + phone + '\'' +
", email='" + email + '\'' +
'}';
}
}
static void runGenericBuilder() {
System.out.println("Generic Builder Pattern Example");
User user = Builder.of(User::new)
.with(User::setName, "John")
.with(User::setLastname, "Doeh")
.with(User::setSecondLastName, "Doeh")
.with(User::setPhone, "555555")
.with(User::setEmail, "mail@mail.com")
.build();
System.out.println(user.toString());
}
3. Builder Design Pattern in Java with Lombok
Lombok is a Java library that eliminates boilerplate code through annotations. Among its many features, it provides a simple way to implement the Builder pattern.
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
@Builder
@Getter
@ToString
public class Videogame {
private String name;
private String platform;
private String category;
}
Usage
static void runLombokBuilder() {
System.out.println("Lombok Builder Pattern Example");
Videogame videogame = Videogame.builder()
.name("The Legend of Zelda")
.platform("Nintendo Switch")
.category("Action-Adventure")
.build();
System.out.println(videogame.toString());
}
Lombok also supports the @Builder annotation on records:
@Builder
public record SoccerTeam(String name, String country, String coach) { }
4. Builder Design Pattern using Records in Java
Records, introduced in Java 14 (preview) and standardized in Java 16, enable the creation of immutable classes without boilerplate. If you want a Builder for a record without Lombok, you can implement it manually:
public record Smartphone(String model, String brand, String operatingSystem, double price) {
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String model;
private String brand;
private String operatingSystem;
private double price;
public Builder model(String model) {
this.model = model;
return this;
}
public Builder brand(String brand) {
this.brand = brand;
return this;
}
public Builder operatingSystem(String operatingSystem) {
this.operatingSystem = operatingSystem;
return this;
}
public Builder price(double price) {
this.price = price;
return this;
}
public Smartphone build() {
return new Smartphone(model, brand, operatingSystem, price);
}
}
}
Usage
static void runRecordBuilder() {
System.out.println("Record Builder Pattern Example");
Smartphone smartphone = Smartphone.builder()
.model("iPhone 14")
.brand("Apple")
.operatingSystem("iOS")
.price(999.99)
.build();
System.out.println(smartphone.toString());
}
Conclusion
Now we know what the Builder design pattern is for and have seen several ways to implement it in Java. Choosing the right approach depends on the context of your project and the specific requirements (e.g., immutability, boilerplate reduction, external libraries).