Builder
Part of the free Low-Level Design course on LogicWiz, in the chapter: Creational patterns. The class-diagram exercises run in the app.
The order with nine arguments
"new Order(cartId, userId, home, null, null, 0, false, true, null). Arjun, which of these nulls is the coupon?"
Priya, reading Arjun's pull request
The problem: a constructor nobody can read
A SwiftKart order has three required parts: the cart, the user and the delivery address. It also has six optional parts:
- a coupon code,
- a delivery slot, for orders scheduled later,
- a tip for the rider,
- contactless delivery,
- cash on delivery,
- a note for the rider, such as "ring the bell twice".
Arjun's first version put all nine in one constructor.
public Order(String cartId, String userId, Address address,
String couponCode, String slotId, long tipPaise,
boolean contactless, boolean cashOnDelivery, String note) {
// copies each argument into a field
}
Order order = new Order("cart-81", "user-7", home, null, null, 0, false, true, null);
Read the last line and try to say what true means. You cannot without opening the class. That is the first problem. There are more:
couponCodeandslotIdare bothStringand sit next to each other. Swap them, and the compiler cannot notice. The coupon "FLAT50" becomes a delivery slot.- Every caller must write every optional value, even the five it does not use.
- SwiftKart has a rule: contactless delivery cannot be paid in cash, because the rider must meet the customer to take the money. Nothing in this constructor checks it.
His second attempt was telescoping constructors: a chain of constructors where each one adds one more argument and passes a default to the next. The name comes from a telescope, whose tubes slide out one after another.
public Order(String cartId, String userId, Address address) {
this(cartId, userId, address, null);
}
public Order(String cartId, String userId, Address address, String couponCode) {
this(cartId, userId, address, couponCode, null);
}
// ... and so on, up to all nine arguments
This helps the simplest callers only. A customer who tips but has no coupon still passes null for the coupon. Six optional parts make 64 possible combinations, and no one writes 64 constructors.
Arjun then said, "Setters do the same job as a builder, with less code. Make an empty constructor and call setCoupon, setTip and so on." Here is what that costs:
- The order exists half built. A caller can forget the address, and nothing complains until delivery.
- The contactless rule has no single place to run. Each setter sees one field at a time.
- The order can change after checkout has charged for it. Someone calls
setCouponlater, and the charged amount no longer matches the order.
The idea: name each part, check once, then build
Think of a sandwich counter. You say the bread, then the filling, then the sauce, one step at a time. Nothing is wrapped until you say "done". If you ask for a combination the shop does not make, they tell you then, before you pay.
In code, put a second object in front of Order whose job is to collect the parts. It has one named method per part, so every call says what it sets. When the caller is done, a final method checks the rules and creates the order in one step. The order is never seen half built.
That second object is a builder, and the pattern is called Builder. Where the analogy stops: the sandwich is assembled as you speak. A builder only records your choices. The real object is created at the end, in one go.
The structure
classDiagram
class Product {
-final fields
-Product(builder)
}
class Builder {
-collected parts
+partA(value) Builder
+partB(value) Builder
+build() Product
}
Client ..> Builder : sets parts
Builder ..> Product : creates
Director ..> Builder : runs fixed steps
The roles:
- Product: the object being built. Here,
Order. Its constructor is private, so the builder is the only way in. - Builder: collects the parts and creates the product in
build(). Here,Order.Builder. - Client: the code that wants a product, such as
CheckoutService. - Director (optional): a class that runs a fixed sequence of builder steps. A
ReorderDirectorcould rebuild last week's order with the same slot and tip.
The original Gang of Four builder leans on the Director. The Director knows the order of the steps, and the builder knows how to assemble the parts. Run the same steps on a different builder, and you get a different result: one builder could write a receipt as plain text and another as HTML. Most Java code today uses the simpler form above, without a Director.
Build it step by step
First, the product. Every field is final, and the only constructor is private and takes the builder.
public final class Order {
private final String cartId, userId, couponCode, slotId, note;
private final Address address;
private final long tipPaise;
private final boolean contactless, cashOnDelivery;
private Order(Builder b) {
this.cartId = b.cartId;
this.userId = b.userId;
this.address = b.address;
this.couponCode = b.couponCode;
this.slotId = b.slotId;
this.tipPaise = b.tipPaise;
this.contactless = b.contactless;
this.cashOnDelivery = b.cashOnDelivery;
this.note = b.note;
}
public static Builder builder(String cartId, String userId, Address address) {
return new Builder(cartId, userId, address);
}
}
The required parts go into builder(...), so nobody can forget them. The builder's constructor can also reject a null address at once, with Objects.requireNonNull(address). Getters such as couponCode() are left out to keep the block short.
Second, the builder. It is a static nested class inside Order, which lets it call the private constructor.
public static final class Builder {
private final String cartId, userId;
private final Address address;
private String couponCode, slotId, note;
private long tipPaise;
private boolean contactless, cashOnDelivery;
private Builder(String cartId, String userId, Address address) {
this.cartId = cartId; this.userId = userId; this.address = address;
}
public Builder coupon(String code) { this.couponCode = code; return this; }
public Builder slot(String id) { this.slotId = id; return this; }
public Builder tip(long paise) { this.tipPaise = paise; return this; }
public Builder contactless() { this.contactless = true; return this; }
public Builder cashOnDelivery() { this.cashOnDelivery = true; return this; }
public Builder note(String text) { this.note = text; return this; }
public Order build() {
if (contactless && cashOnDelivery) {
throw new IllegalStateException("Contactless orders cannot be paid in cash");
}
return new Order(this);
}
}
Each step method returns this, the builder itself, so calls can be chained one after another. This style is called a fluent interface. build() checks the rules that involve more than one field. It runs once, after every part is known, so the order of the calls does not matter.
Where should each check live? A step can reject a value that is wrong on its own, such as a negative tip: tip(-5) can throw at once. A rule across fields must wait for build(). When contactless() runs, the builder cannot know whether cashOnDelivery() comes next.
Third, the call site now reads like a sentence.
Order order = Order.builder("cart-81", "user-7", home)
.coupon("FLAT50")
.tip(2000)
.contactless()
.build();
The built Order is immutable: once created, it cannot change. It has no setters, and its fields are final. Checkout can charge for it, and the charged order is the order you keep. An immutable object is also safe to share between threads with no locks, which matters in the concurrency chapter. The builder itself is not immutable. Use one builder per order, and do not share it.
One trap remains. Suppose Order also held a List<Item>. If the constructor stores the caller's list as it is, the caller still holds the same list and can add items after checkout. Copy it with List.copyOf(items), which returns a list nobody can change.
When to use it, and when it is overkill
A builder pays off when:
- an object has several optional parts, roughly four or more arguments in total,
- some rules involve more than one field,
- you want the result to be immutable,
- tests create many variations of one object. A test can write
Order.builder(...).contactless().build()and show only what matters.
Some languages need it less. Kotlin and Python have named arguments with default values, so Order(cartId, userId, home, tip = 2000) is already clear. Java has neither, which is why builders are so common in Java code.
It is overkill for a class with two or three required fields and nothing optional. Money has an amount and a currency. Use a constructor, or a Java record: a short class declaration where Java writes the final fields, constructor and getters for you. record Money(long paise, String currency) {} is one line, and its constructor can still validate.
The cost is a second class that must stay in step with the first. Add a giftWrap field to Order and forget the builder, and no caller can ever set it.
How it differs from the factories in the last two lessons:
- A factory answers "which class?" in one call.
- A builder answers "how do I assemble this one object from many parts?" over several calls.
- An abstract factory returns several matching objects, each ready at once. A builder returns one object, at the end.
You have used builders already. Java's StringBuilder collects text step by step. HttpRequest.newBuilder() builds an HTTP request the same way. In interviews, a builder fits a food order with toppings, or a hotel or movie booking with optional add-ons such as snacks. A builder for the one complex object is a good touch in a machine-coding round. A builder for every class is noise.
Arjun's Order still has one long constructor. Add a builder with a named step for each optional part and a build method that creates the order.
📌 Summary: Use a builder when an object has many optional parts: required parts up front, a named step per option, rules checked once in build(), and an immutable result.
What Arjun learns next
Every order also needs an ID, and two orders must never share one. Arjun's answer is a class that allows only one instance of itself. Priya has questions. Next: the singleton.
Check your understanding
- An Order has 3 required fields, 6 optional ones, and a rule linking two of them. Which design fits best?
- Money has exactly two required fields, amount and currency, and no optional ones. Should it have a builder?
- Why does the builder check "no cash on delivery for contactless orders" in build(), not inside contactless()?
- Arjun says setters do the same job as a builder with less code. What does the builder give that setters do not?