Definition
- 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 fieldsfinal. - 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.
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?
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?
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);
}
}
ORD-001: BUY 100 RELIANCE @ 2450.00 fee=50.00 acc=ACC-RAJESH
How do you design a Builder for a query?
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 + "]";
}
}
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);
}
}
Query[ACC-PRIYA 2024-01-01..2024-03-31 types=[DEBIT, TRANSFER] limit=50]
How does build() enforce validation?
try {
Order bad = Order.builder()
.orderId("ORD-002")
.quantity(-5) // invalid
.build();
} catch (IllegalStateException e) {
System.out.println("Rejected: " + e.getMessage());
}
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.
// 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?
StringBuilderchainsappend()calls.HttpRequest.newBuilder()in Java 11+.- Spring
UriComponentsBuilder. - Lombok
@Builderannotation.
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
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.
---