Home › Low-Level Design › Builder Pattern
B

Builder Pattern

Builder lets you assemble a complicated object step by step, then hand back a finished, valid instance in one shot. I reach for it the moment a constructor starts collecting too many arguments, or an object needs to be immutable but still pleasant to configure. The candidates who pass do not recite the structure. They name the pain it solves, put required fields where you cannot forget them, and say the one place it gets misused before the interviewer asks.

Low-Level DesignMedium10 min read

If you remember five things

The whole pattern in one glance. Everything below defends these five calls.

  1. Intent: separate assembly from the thing. A mutable builder owns the messy step-by-step configuration; the product owns being a valid, finished object.
  2. When to reach for it. Many fields, several optional, and you want the result immutable. The telescoping constructor and the half-built setter object are the two pains it kills.
  3. Structure. A Product with a private constructor, a nested Builder with chainable setters that return `this`, and a terminal `build()`. The Director is optional and usually skipped.
  4. Validation lives in build(). Required fields and cross-field rules get checked once, on the whole object, so nothing escapes the builder in a broken state.
  5. The classic gotcha: confusing it with Factory. Factory picks WHICH class to make. Builder assembles ONE known class. Reaching for a builder on a clean three-arg constructor is the other giveaway.

1The problem it solves#

Start with the pain, because the pattern only makes sense once you feel it. Picture a Pizza class: size, crust, cheese, up to six toppings, a sauce, and a flag for extra-well-done. Most of those are optional. So what does the constructor look like?

new Pizza("large", "thin", true, "pepperoni", null, null, "marinara", false);

This is unreadable at the call site.

  • Quick: what does the third true mean? What is that false at the end?
  • You cannot tell without jumping to the class definition.
  • This is the telescoping constructor problem: as options grow, you write a dozen overloaded constructors or one giant constructor full of nulls and booleans.
  • With eight parameters and four optional, the plausible overloads explode past what anyone wants to maintain.

The usual escape hatch is setters, and it quietly makes things worse.

  • Create the object empty, then call setSize, setCrust, and so on. Reads fine.
  • But now the object is mutable, and worse, it can exist in a half-built state.
  • Between new Pizza() and the last setter you are holding an invalid object.
  • Another thread grabs it, or you forget a required field, and you ship a bug.
  • It throws away the immutability you usually want from a value object.

Builder gives you both halves at once.

  • Readable construction AND a fully-formed, often immutable, result.
  • You configure on a separate builder object, then call build() once.
  • The real object is never seen in a partial state.
The one sentence to open with

"A builder is a mutable scratchpad whose only job is to produce one immutable thing." Say that and you have framed the entire pattern. Everything else (fluent setters, validation in build, defensive copies) is just defending that sentence.

2The idea (mental model)#

Separate how an object is configured from what the object is.

  • That sentence is the Single Responsibility Principle applied to construction.
  • The builder owns the messy, step-by-step assembly.
  • The product owns being a valid, finished thing.

The sandwich analogy makes it stick.

  • You do not reach behind the counter and assemble a sandwich yourself.
  • You tell the person: wheat bread, turkey, no onions, extra mustard. That is you talking to a builder.
  • When you finish listing, they hand you the finished sandwich. You never touched a half-made one.

Two properties fall out of this.

  • Each configuration step returns the builder itself, so calls chain: .size("large").cheese(true). That is the fluent interface, and it is what makes Builder pleasant to read.
  • Validation lives in build(). That one method is your gate. Required fields missing? Throw there. Illegal combination? Reject there. Nothing escapes the builder broken.

3Structure#

The cast is small, so name it fast.

  • Product. The thing you actually want. Immutable, with a private constructor only the builder can call.
  • Builder. Holds the in-progress fields, exposes chainable setters, and has a terminal build(). In Java it is almost always a static nested class inside the product, because it needs the product's private constructor and nothing else should.
  • Director (optional). In the classic Gang of Four version, an object that knows a fixed recipe and drives the builder through it. It earns its keep only when you have a few standard configurations you want to name (think "build me the standard margherita"). I rarely use one in interviews.
classDiagram class Pizza { -String size -String crust -boolean cheese -List~String~ toppings -Pizza(builder) } class PizzaBuilder { -String size -String crust -boolean cheese -List~String~ toppings +size(s) PizzaBuilder +crust(c) PizzaBuilder +cheese(on) PizzaBuilder +addTopping(t) PizzaBuilder +build() Pizza } class PizzaDirector { +margherita(builder) Pizza } Pizza +-- PizzaBuilder PizzaDirector --> PizzaBuilder PizzaBuilder --> Pizza
The relationships at a glance. The arrow that matters is PizzaBuilder to Pizza: the builder produces the product. The Director, when present, only orchestrates the builder, so drop it from the drawing unless named presets come up.

4Implementation in Java#

Here is the shape I write on the board. Watch three things as you read it: the private constructor, the nested builder, and validation that lives in build().

public final class Pizza {
    private final String size;      // required
    private final String crust;     // required
    private final boolean cheese;   // optional
    private final List<String> toppings;

    // Only the builder can call this.
    private Pizza(Builder b) {
        this.size = b.size;
        this.crust = b.crust;
        this.cheese = b.cheese;
        this.toppings = List.copyOf(b.toppings); // defensive copy, now immutable
    }

    public static Builder builder(String size, String crust) {
        return new Builder(size, crust); // required fields up front
    }

    public static final class Builder {
        private final String size;
        private final String crust;
        private boolean cheese = false;
        private final List<String> toppings = new ArrayList<>();

        private Builder(String size, String crust) {
            this.size = size;
            this.crust = crust;
        }

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

        public Builder addTopping(String t) {
            this.toppings.add(t);
            return this;
        }

        public Pizza build() {
            if (size == null || crust == null) {
                throw new IllegalStateException("size and crust are required");
            }
            if (toppings.size() > 6) {
                throw new IllegalStateException("max 6 toppings");
            }
            return new Pizza(this);
        }
    }
}

Usage reads like a sentence:

Pizza p = Pizza.builder("large", "thin")
               .cheese(true)
               .addTopping("pepperoni")
               .addTopping("mushroom")
               .build();

The one design note worth saying out loud.

  • I forced size and crust into builder(...) as constructor args because they are required.
  • That means you cannot even start building without them. The optional stuff gets fluent setters.
  • This single move kills most "forgot a required field" bugs before build() ever runs.
  • It also pushes a whole class of errors from runtime to compile time, which is exactly the direction you want.

If you want named recipes, the Director is tiny:

public final class PizzaDirector {
    public static Pizza margherita() {
        return Pizza.builder("medium", "thin")
                    .cheese(true)
                    .addTopping("basil")
                    .build();
    }
}

5A real-world example#

The one I point to first is StringBuilder.

  • You do not build a String by concatenating in a loop, because String is immutable and each + allocates a brand new object.
  • For a 1,000-iteration loop that is roughly 1,000 throwaway allocations and quadratic copying.
  • You append to a mutable builder, then call toString() once.
  • Same bones as our pattern: a mutable accumulator that yields one immutable result.

Beyond the JDK, the pattern is everywhere in real systems.

  • HTTP clients. You configure a request with .url(...).header(...).timeout(...).build(). The request object is immutable, which matters because it might be retried, logged, or passed between threads. Java 11's own HttpRequest.newBuilder() is exactly this.
  • Query assembly. SELECT-style builders chain .from(...).where(...).orderBy(...) and produce a finished query string or object.
  • Config objects. Anything with ten knobs, most optional, with sane defaults: a server config, a connection pool, a retry policy.

In LLD rounds specifically, I have seen Builder requested or rewarded in:

  • A Notification with optional channels and attachments.
  • A Meal or Burger customization system.
  • An HttpRequest, a Report with many sections, a document or UI-component tree.
  • If the problem statement says "many optional fields" or "configure, then create," your interviewer is fishing for this.

6When to use it, and when not to#

Use it when:

  • The object has many fields and several are optional. My rough rule: more than four constructor args, or more than one or two optional ones, and I start thinking Builder.
  • You want the result immutable but still convenient to configure.
  • Construction needs validation that only makes sense on the whole object, not field by field.
  • You find yourself writing two or more overloaded constructors just to cover combinations.

Skip it when:

  • The object has two or three fields and they are all required. A plain constructor is clearer. Do not add a builder for a Point(x, y).
  • A Java record already does the job. Records give you a clean immutable value type for free. No optional fields with defaults or cross-field validation? A record beats a builder on effort.
  • You are tempted to add a Director for a single configuration. A Director with one recipe is just indirection. Only add it when named presets genuinely multiply.
~2x
The cost you are payingBuilder roughly doubles the code for a class. You buy readability and immutability with boilerplate. For a two-field class that is a bad trade. For a twelve-field config it pays for itself immediately. Name the trade-off out loud and the interviewer knows you are choosing, not reflexing.

7Common mistakes#

Each of these is a real red flag a senior interviewer watches for, so recognize them in your own code before they do.

Letting the product escape half-built.

  • If your builder stores a reference to a live Pizza and mutates it, you have reinvented the setter problem.
  • Build into plain fields on the builder, then construct the product once in build().

No defensive copy of collections.

  • In my code I wrote List.copyOf(b.toppings).
  • Skip that and the caller still holds a reference to the internal list through the builder, so they can mutate "your" immutable pizza after the fact.
  • Copy mutable inputs on the way in.

Validation in the setters instead of build().

  • Throw inside addTopping based on how many have been added so far, and you tie validity to call order.
  • Cross-field rules belong in build(), where you see the finished picture.

Forgetting to return this.

  • Easy to do when typing fast. One setter returns void and the whole chain breaks at compile time. Interviewers notice.

Making the builder's own fields final by reflex.

  • The builder is the mutable part. Its optional fields need to change as you chain.
  • Only the required ones passed in the builder's constructor should be final.
Red flag that fails you here

Reaching for Builder on everything. A builder wrapped around a clean three-arg constructor signals you are pattern-matching instead of thinking. The strong move is to name the trade-off ("this doubles the code for no optional fields, so a plain constructor or record is better here") and move on. Candidates who bolt a builder onto a Point(x, y) are telling me they memorized the pattern without understanding the pain it solves.

8Where it shows up in interviews#

Three ways it comes up.

  • Directly. "Design a Pizza / Burger / HttpRequest / Notification with these optional fields." Builder is the expected answer. Lead with the telescoping-constructor pain, then show the fluent builder with required fields in the constructor and validation in build().
  • As a follow-up. You have designed something with a messy constructor and the interviewer asks, "how would you make this cleaner, immutable, or thread-safe to construct?" That is your cue.
  • As a "which pattern" question. They describe many optional fields and you name it.
Pause before you read on: the interviewer says "isn't this just a Factory?" What is the one-line answer that keeps you out of the ditch?
Reveal the answer
No. A Factory decides WHICH class to instantiate and hides that choice behind a method. A Builder assembles ONE known class step by step. Factory is about type selection, Builder is about complex construction. Different jobs. Mixing these two up is the single most common way candidates lose the plot on this pattern, so have the distinction ready before they ask.

The thing that separates a strong answer: do not just recite the structure. Say WHY.

  • "I put required fields in the builder constructor so you cannot forget them."
  • "I validate in build() so the object is never partially valid."
  • "I copy the list so the result is genuinely immutable."
  • That reasoning is what gets the check mark. At a senior loop it is the difference between "knows the pattern" and "knows when the pattern is wrong."

Here is the one runtime flow worth having in your head.

sequenceDiagram participant Client participant Builder participant Pizza Client->>Builder: builder("large", "thin") Client->>Builder: cheese(true) Client->>Builder: addTopping("pepperoni") Client->>Builder: build() Builder->>Builder: validate fields Builder->>Pizza: new Pizza(this) Pizza-->>Client: finished immutable Pizza
The runtime flow. Notice the client only ever talks to the builder until the very last step, when a complete, validated, immutable Pizza lands in its hands. The product is never visible in a partial state, and that is the whole point.

A builder is a mutable scratchpad whose only job is to produce one immutable thing.

Follow-ups the interviewer will ask (and your answers)
"How is this different from a Factory?"
A Factory decides which class to instantiate and hides that choice. A Builder assembles one known class step by step. Factory is about type selection, Builder is about complex construction. If someone hands you "many optional fields, configure then create," that is Builder, not Factory.
"Why not just use setters and skip the builder?"
Setters make the object mutable and let it exist half-built. Between `new` and the last setter you are holding an invalid object that another thread could grab, or you forget a required field and ship a bug. Builder keeps the product immutable and never exposes a partial state, because configuration happens on a separate scratchpad and the real object only appears after `build()`.
"Where exactly should validation go, and why?"
In `build()`, not the setters. Cross-field rules (required fields present, illegal combinations) only make sense on the finished object. Validating in a setter ties correctness to call order, so a rule like "max 6 toppings" fires at the wrong moment and misses combinations you only see once everything is set.
"When would you NOT use Builder?"
Two or three required fields: a plain constructor is clearer, so I would not builderize a `Point(x, y)`. If a Java record covers it (immutable value type, no optional defaults, no cross-field validation), the record wins on effort. And I would not add a Director for a single recipe, because one preset is just indirection. Builder roughly doubles the code, so I only pay that for a genuinely multi-field, mostly-optional object.
"How do you guarantee the result is actually immutable?"
Private final fields, a private constructor only the builder calls, and a defensive copy of every mutable input. In my code `List.copyOf(b.toppings)` is what stops the caller from mutating the internal list through the builder after `build()` returns. Skip that copy and "immutable" is a lie.

What you can now explain in the room

  • The telescoping-constructor pain and why setters make it worse, not better.
  • The one-sentence intent: a mutable scratchpad that produces one immutable thing.
  • The structure (Product, nested Builder, optional Director) and which arrow actually matters.
  • Why required fields go in the builder's constructor and validation goes in `build()`.
  • The exact difference between Builder and Factory, and why people confuse them.
  • When to skip it: few required fields, a record fits, or a Director with one recipe.

Next, look at the Factory Method and Abstract Factory guides to see the "which object" side of creation, and try the "Design a Notification System" LLD problem where Builder and Strategy often show up together.

Get a prep plan built around your Builder Pattern interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Builder Pattern prep plan is on the way.