Back to Creational: Singleton, Factory, Builder, Prototype
Q10Question 10 of 12

Design using the Builder pattern

Key Concepts
  • Builder separates construction from representation, producing an immutable, validated object.
  • Used for objects with many optional fields: orders, queries, configurations.
  • Fluent interface: each setter returns this, enabling readable chained calls ending in build().
  • build() validates invariants; invalid objects never exist.
  • Required fields go in the builder's constructor; optional fields get fluent setters with defaults.
  • Lombok @Builder generates the pattern, but understanding the hand-written form is essential for interviews.

Definition

The Builder pattern constructs a complex object step by step through a fluent interface, producing an immutable, consistent result.
  • Builder pattern – separates the construction of a complex object from its representation, letting you build it step by step.
  • Fluent interface – each setter returns this, enabling readable chained calls.
  • Immutable result – build() produces a final object with all fields final.
  • Validation – build() enforces invariants; invalid objects never exist.
  • Use cases – orders, queries, configurations, any object with many optional fields.

Think of the Builder as filling out a detailed account-opening form at National Bank of India one field at a time. Rajesh fills in name, then account type, then optional nominee, then optional overdraft limit, and finally submits. Instead of handing a clerk fifteen values in a fixed order, he fills named fields step by step and creates the account only when the form is complete.

Understanding the Concepts

What problem does Builder solve?

The telescoping-constructor problem: an object with many fields forces either a huge constructor whose arguments are easy to misorder, or many overloaded constructors.

JAVA
package com.bank.model;

import java.math.BigDecimal;

// BAD: positional arguments, easy to misorder
// new Order("ORD-1", "ACC-001", "RELIANCE", 100, price, fee, "BUY", notes)
// Was 100 the quantity or the account number?

How do you design a Builder for a trade order?

JAVA
package com.bank.model;

import java.math.BigDecimal;

public final class Order {
    private final String orderId;
    private final String accountId;
    private final String symbol;
    private final int quantity;
    private final BigDecimal price;
    private final BigDecimal fee;
    private final String orderType;
    private final String notes;

    private Order(Builder b) {
        this.orderId = b.orderId;
        this.accountId = b.accountId;
        this.symbol = b.symbol;
        this.quantity = b.quantity;
        this.price = b.price;
        this.fee = b.fee;
        this.orderType = b.orderType;
        this.notes = b.notes;
    }

    public static Builder builder() { return new Builder(); }

    public static class Builder {
        private String orderId;
        private String accountId;
        private String symbol;
        private int quantity;
        private BigDecimal price;
        private BigDecimal fee = BigDecimal.ZERO;
        private String orderType = "BUY";
        private String notes = "";

        public Builder orderId(String v) { this.orderId = v; return this; }
        public Builder accountId(String v) { this.accountId = v; return this; }
        public Builder symbol(String v) { this.symbol = v; return this; }
        public Builder quantity(int v) { this.quantity = v; return this; }
        public Builder price(BigDecimal v) { this.price = v; return this; }
        public Builder fee(BigDecimal v) { this.fee = v; return this; }
        public Builder orderType(String v) { this.orderType = v; return this; }
        public Builder notes(String v) { this.notes = v; return this; }

        public Order build() {
            if (orderId == null) throw new IllegalStateException("orderId required");
            if (accountId == null) throw new IllegalStateException("accountId required");
            if (symbol == null) throw new IllegalStateException("symbol required");
            if (quantity <= 0) throw new IllegalStateException("quantity must be positive");
            if (price == null || price.signum() <= 0) throw new IllegalStateException("price must be positive");
            return new Order(this);
        }
    }

    @Override
    public String toString() {
        return orderId + ": " + orderType + " " + quantity + " " + symbol
            + " @ " + price + " fee=" + fee + " acc=" + accountId;
    }
}

How do you use the Builder?

JAVA
package com.bank.demo;

import java.math.BigDecimal;

public class OrderBuilderDemo {
    public static void main(String[] args) {
        Order order = Order.builder()
            .orderId("ORD-001")
            .accountId("ACC-RAJESH")
            .symbol("RELIANCE")
            .quantity(100)
            .price(new BigDecimal("2450.00"))
            .fee(new BigDecimal("50.00"))
            .orderType("BUY")
            .notes("Market order at open")
            .build();

        System.out.println(order);
    }
}
TEXT
ORD-001: BUY 100 RELIANCE @ 2450.00 fee=50.00 acc=ACC-RAJESH

How do you design a Builder for a query?

JAVA
package com.bank.service;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

public final class TransactionQuery {
    private final String accountId;
    private final LocalDate from;
    private final LocalDate to;
    private final List<String> types;
    private final int limit;

    private TransactionQuery(Builder b) {
        this.accountId = b.accountId;
        this.from = b.from;
        this.to = b.to;
        this.types = List.copyOf(b.types);
        this.limit = b.limit;
    }

    public static Builder builder(String accountId) {
        return new Builder(accountId);
    }

    public static class Builder {
        private final String accountId; // required
        private LocalDate from = LocalDate.now().minusDays(30);
        private LocalDate to = LocalDate.now();
        private List<String> types = new ArrayList<>();
        private int limit = 100;

        private Builder(String accountId) {
            this.accountId = accountId;
        }

        public Builder from(LocalDate v) { this.from = v; return this; }
        public Builder to(LocalDate v) { this.to = v; return this; }
        public Builder type(String v) { this.types.add(v); return this; }
        public Builder limit(int v) { this.limit = v; return this; }

        public TransactionQuery build() {
            if (from.isAfter(to)) throw new IllegalStateException("from must be before to");
            return new TransactionQuery(this);
        }
    }

    @Override
    public String toString() {
        return "Query[" + accountId + " " + from + ".." + to + " types=" + types + " limit=" + limit + "]";
    }
}
JAVA
package com.bank.demo;

import java.time.LocalDate;

public class QueryBuilderDemo {
    public static void main(String[] args) {
        TransactionQuery query = TransactionQuery.builder("ACC-PRIYA")
            .from(LocalDate.of(2024, 1, 1))
            .to(LocalDate.of(2024, 3, 31))
            .type("DEBIT")
            .type("TRANSFER")
            .limit(50)
            .build();

        System.out.println(query);
    }
}
TEXT
Query[ACC-PRIYA 2024-01-01..2024-03-31 types=[DEBIT, TRANSFER] limit=50]

How does build() enforce validation?

JAVA
try {
    Order bad = Order.builder()
        .orderId("ORD-002")
        .quantity(-5) // invalid
        .build();
} catch (IllegalStateException e) {
    System.out.println("Rejected: " + e.getMessage());
}
TEXT
Rejected: symbol required

Invalid objects can never exist.

How do you separate required from optional fields?

Required fields go in the builder's constructor; optional fields get fluent setters with defaults.

JAVA
// Required in builder constructor:
public static Builder builder(String accountId) {
    return new Builder(accountId); // cannot be forgotten
}

// Optional with defaults:
private int limit = 100;
public Builder limit(int v) { this.limit = v; return this; }

Where does Builder appear in the real world?

  • StringBuilder chains append() calls.
  • HttpRequest.newBuilder() in Java 11+.
  • Spring UriComponentsBuilder.
  • Lombok @Builder annotation.

What should a senior engineer know?

  • Builder shines for many optional fields and immutability with validation.
  • For two required fields, a plain constructor is simpler.
  • In banking, a validated, immutable order means the object is correct from birth and cannot be tampered with after creation.

Visual Overview

DIAGRAM
  Builder Pattern – Three Use Cases
  ──────────────────────────────────

  ORDER:
  Order.builder()
    .orderId("ORD-001")     named fields
    .symbol("RELIANCE")     no positional guessing
    .quantity(100)          optional fields have defaults
    .price(2450)
    .build()                validates → immutable Order

  QUERY:
  TransactionQuery.builder("ACC-PRIYA")   required in constructor
    .from(2024-01-01)                     optional
    .type("DEBIT")                        additive
    .limit(50)                            optional
    .build()                              validates → immutable Query

  CONFIG:
  AppConfig.builder()
    .dbUrl("...")           required
    .poolSize(10)           optional with default
    .timeout(30)            optional with default
    .build()                validates → immutable Config

Key Takeaway

The Builder pattern separates the construction of a complex object from its representation, letting Rajesh assemble it step by step through a fluent interface and produce an immutable, validated result. He reaches for it whenever an object has many fields, especially several optional ones, because the alternatives are both bad: one enormous constructor with positional arguments, or a tangle of overloaded constructors.

At National Bank of India, Rajesh builds trade orders, transaction queries, and configuration objects this way. Each has required fields that go into the builder's constructor (so they cannot be forgotten) and optional fields with fluent setters and sensible defaults. The build() method validates invariants, so an order with a missing symbol or negative quantity can never exist.

The fluent interface works because each setter returns this, enabling chained calls that read almost like a sentence. The final build() creates the real object with all final fields, which is immutable and safe to share across settlement, audit, and notification threads without locks.

The design judgment is that Builder earns its place for many optional fields and immutability with validation. For a class with two or three required fields, a plain constructor is simpler and clearer.

Remember one sentence: Builder replaces unreadable multi-parameter constructors with named, chained setters and a validating build() that produces an immutable, correct-by-construction object.

---

Share
Use ← → arrow keys to navigate