The Builder Design Pattern is a creational design pattern that lets you construct complex objects step by step. Using Builder pattern you can use same construction process to can create different representations.

Use case

Use the Builder Pattern when:

  • An object has many optional fields or configurations.

  • You want to avoid constructor telescoping (i.e., too many overloaded constructors).

  • You need to create immutable objects step by step.

public class User {
    private String firstName;
    private String lastName;
    private int age;
    private String email;
    private String phone;

    public User(String firstName, String lastName) { ... }
    public User(String firstName, String lastName, int age) { ... }
    public User(String firstName, String lastName, int age, String email) { ... }

    // ...
}

In above example, you can see User class is hard to maintain and read.

Core Concepts

  1. Product: The complex object being built. It’s typically a class with many fields.
  2. Builder: An interface or abstract class that defines the steps to build the product.
  3. ConcreteBuilder: A class that implements the Builder interface and provides specific implementation for building the product. It keeps track of the object being built and provides a method (build) to return the final product.

Note: The Builder pattern can be implemented with or without a formal Builder interface or abstract class. The core concept is the separate builder object, not the strict use of an interface.

Implementation

public class User {
    // Required fields 
    private final String firstName;
    private final String lastName;

    // Optional fields
    private final Integer age;
    private final String email;
    private final String phone;

    private User(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.email = builder.email;
        this.phone = builder.phone;
    }

    // Builder Class
    public static class Builder {
        private final String firstName;
        private final String lastName;

        private Integer age;
        private String email;
        private String phone;

        public Builder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

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

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

        public User build() {
            return new User(this);
        }
    }

    @Override
    public String toString() {
        return "User{" +
                "firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", age=" + age +
                ", email='" + email + '\'' +
                ", phone='" + phone + '\'' +
                '}';
    }
}

Client Code:

public class Main {
    public static void main(String[] args) {
        User user = new User.Builder("John", "Doe")
                .age(30)
                .email("john@example.com")
                .phone("1234567890")
                .build();

        System.out.println(user);
    }
}

Real world examples

  • StringBuilder (JDK)
  • java.time.LocalDateTime.Builder
  • AlertDialog.Builder in Android
  • Libraries like Lombok, Project Lombok @Builder

Drawbacks

  • More boilerplate code
  • Verbose
  • Testing complexity
  • Cannot enforce field dependencies easily

Kotlin Implementation (using DSL)

You can simplify builder creation using Kotlin’s function literals with receivers:

data class User(
    val firstName: String,
    val lastName: String,
    var age: Int? = null,
    var email: String? = null,
    var phone: String? = null
)

class UserBuilder(private val firstName: String, private val lastName: String) {
    var age: Int? = null
    var email: String? = null
    var phone: String? = null

    fun build() = User(firstName, lastName, age, email, phone)
}

fun buildUser(firstName: String, lastName: String, block: UserBuilder.() -> Unit): User {
    val builder = UserBuilder(firstName, lastName)
    builder.block()
    return builder.build()
}

Client Code

val user = buildUser("Jane", "Doe") {
    age = 28
    email = "jane@example.com"
    phone = "012356789"
}