🏗️ Builder Pattern

Quick Revision Guide

🧠 Core Concept

The "Checklist" Analogy

Instead of forcing every possible combination of an object into separate constructors (like a massive, confusing restaurant menu), the Builder Pattern acts as a checklist. You build your object step-by-step and "bake" it at the end.

🚨 The Problem

Telescoping Constructors

Creating objects with many optional fields traditionally requires massive, unreadable constructors.

// Hard to read, easy to mix up arguments, forces passing `null` or `false`
Car myCar = new Car("V8", true, false, true, 4, "Red", false);

🛠️ The Solution & Implementation Rules

You build the object incrementally and call a build() method to lock it in.

Key Implementation Rules in Java:

  • Private & Final Fields: The target class (e.g., Car) fields are private and final to ensure immutability once built.
  • Private Constructor: The target class constructor must be private so external code cannot bypass the builder. It takes the Builder as its only parameter.
  • Getters Only: The target class provides getters but no setters (read-only).
  • Return this: Builder configuration methods must return this to allow method chaining (e.g., .setEngine("V8").addSunroof()).
  • The build() Method: The final step inside the Builder that returns new Car(this).

🏗️ Why a Static Nested Class?

The standard in Java is to make the Builder a static inner class (e.g., public static class CarBuilder) for three main reasons:

  • Access: A nested class has full access to the private constructor of the outer Car class.
  • Independence: Because it is static, you can instantiate CarBuilder without needing a pre-existing instance of Car (new Car.CarBuilder()).
  • Organization: It logically groups the builder with its target class, avoiding namespace clutter in your project folders.

💻 Quick Code Skeleton

public class Car {
    private final String engine;
    
    // 1. Private constructor accessed only by Builder
    private Car(CarBuilder builder) { 
        this.engine = builder.engine;
    }
    
    public String getEngine() { return engine; }
    
    // 2. Static Nested Class
    public static class CarBuilder { 
        private String engine;
        
        // 3. Returns 'this' for method chaining
        public CarBuilder setEngine(String engine) {
            this.engine = engine;
            return this; 
        }
        
        // 4. Build method returns the final object
        public Car build() {
            return new Car(this);
        }
    }
}

Usage Example:

Car myCar = new Car.CarBuilder() .setEngine("V8") .build();

🌐 Enterprise Example: SCIM User Object

In real enterprise applications (like identity management, directory synchronization, and security automation), objects can become incredibly complex.

Below is a robust Java implementation of a SCIM User Object. It demonstrates three advanced, real-world patterns:

  1. Mandatory Fields: Enforced via the Builder's constructor (you can't start building a SCIM User without a userName).
  2. Sensible Defaults: Setting active = true implicitly unless overridden.
  3. Collection Management & Immutability: Dynamically appending to lists (emails, roles) and returning unmodifiable collections to guarantee safety.

Complete SCIM User Implementation:

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class ScimUser {
    // Immutable properties
    private final String id;            // Optional (assigned by identity provider)
    private final String userName;      // MANDATORY
    private final String externalId;    // Optional
    private final boolean active;       // Optional (Defaults to true)
    private final List<String> emails;  // Multi-valued
    private final List<String> roles;   // Multi-valued

    // 1. Private constructor maps Builder values to final fields
    private ScimUser(Builder builder) {
        this.id = builder.id;
        this.userName = builder.userName;
        this.externalId = builder.externalId;
        this.active = builder.active;
        
        // Defensive copying to prevent external mutation of the lists
        this.emails = builder.emails != null ? 
                Collections.unmodifiableList(new ArrayList<>(builder.emails)) : Collections.emptyList();
        this.roles = builder.roles != null ? 
                Collections.unmodifiableList(new ArrayList<>(builder.roles)) : Collections.emptyList();
    }

    // Getters only (No setters to ensure thread safety and immutability)
    public String getId() { return id; }
    public String getUserName() { return userName; }
    public String getExternalId() { return externalId; }
    public boolean isActive() { return active; }
    public List<String> getEmails() { return emails; }
    public List<String> getRoles() { return roles; }

    @Override
    public String toString() {
        return "ScimUser [id=" + id + ", userName=" + userName + ", externalId=" + externalId + 
               ", active=" + active + ", emails=" + emails + ", roles=" + roles + "]";
    }

    // 2. Static Inner Builder
    public static class Builder {
        private String id;
        private final String userName; // Final inside builder enforces it's set at creation
        private String externalId;
        private boolean active = true; // Sensible default state
        private final List<String> emails = new ArrayList<>();
        private final List<String> roles = new ArrayList<>();

        // Enforce mandatory parameters through the Builder's constructor
        public Builder(String userName) {
            if (userName == null || userName.isBlank()) {
                throw new IllegalArgumentException("Mandatory field 'userName' cannot be null or empty.");
            }
            this.userName = userName;
        }

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

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

        public Builder active(boolean active) {
            this.active = active;
            return this;
        }

        // Methods to handle multi-valued SCIM fields cleanly
        public Builder addEmail(String email) {
            if (email != null && !email.isBlank()) {
                this.emails.add(email);
            }
            return this;
        }

        public Builder addRole(String role) {
            if (role != null && !role.isBlank()) {
                this.roles.add(role);
            }
            return this;
        }

        // Finalize construction
        public ScimUser build() {
            return new ScimUser(this);
        }
    }
}

🎮 How to Use the SCIM Builder:

public class ScimDemo {
    public static void main(String[] args) {
        // Build a comprehensive, read-only Enterprise SCIM User
        ScimUser user = new ScimUser.Builder("john.doe@company.com") // Enforces mandatory username
                .id("usr_9f830a1b")
                .externalId("emp_12345")
                .active(true)
                .addEmail("john.doe@company.com")
                .addEmail("j.doe@personal.com")
                .addRole("User")
                .addRole("Administrator")
                .build(); // Locks the object in place

        System.out.println(user);
        // Output: ScimUser [id=usr_9f830a1b, userName=john.doe@company.com, ...
    }
}
Key Takeaways: This enterprise example showcases how the Builder Pattern scales to handle mandatory fields, defaults, collection management, and true immutability—making it the gold standard for creating safe, maintainable objects in production systems.