Beginner14 min readDesign Patternslive prototype

Builder

Build a complex object one clear step at a time instead of jamming everything into one giant constructor. You call small named methods like `.addCheese()` and finish with `.build()`, so the code reads like a recipe and you can skip the parts you don't want.

The idea

What it is

The Builder pattern helps you create a complex object one readable step at a time instead of passing every option into one big constructor at once. You call small, clearly named methods — .addPatty(), .addCheese(), .addBacon() — and then call .build() to get the finished object back. Each step is optional and the order is flexible, so the code reads like a recipe.

Think about building a burger. A burger has a bun and then a bunch of optional parts: a patty, cheese, bacon, lettuce, tomato, sauce, maybe a second patty. If you tried to make one with a single constructor, you'd end up writing new Burger(true, false, true, true, false, true, false) — and nobody looking at that line can tell which true is the cheese and which is the bacon. With a Builder you write new BurgerBuilder().addPatty().addCheese().addBacon().build() instead. Now every part is named, you only mention the parts you actually want, and the line reads out loud like an order at the counter.

The one sentence to remember

A Builder lets you assemble an object step by step with named methods and finish with .build(), so a pile of confusing constructor arguments becomes a self-explaining chain: new BurgerBuilder().addCheese().addBacon().build().

The problem it kills: the telescoping constructor

When a class has many optional fields, people tend to add more and more constructor arguments (or a growing stack of overloaded constructors) until you get a long row of true/false/null values that only makes sense if you count the positions. That mess is called the telescoping constructor (also nicknamed param soup). The Builder replaces that unreadable row of arguments with a chain of named steps.

Mechanics

How it works

The two pieces: a Product and its Builder

A Builder setup has just two moving parts. The Product is the thing you actually want (the finished Burger). The Builder is a small helper object that collects your choices and then hands you the finished product:

  • The Builder holds the choices so far. As you call .addCheese() or .addBacon(), the builder just records that choice inside itself. Nothing is finished yet — you're still gathering ingredients.
  • Each step returns `this`. Every method ends by returning the builder itself, so you can immediately call the next method on it. That's the trick that lets the calls chain into one readable line — it's called a fluent interface.
  • `build()` returns the finished Product. The very last call takes everything the builder gathered and constructs the real Burger from it, then hands it back. Ideally the returned burger is immutable — done and unchangeable — so nobody can quietly alter it afterwards.
typescript
class BurgerBuilder {
  private parts: string[] = ["bun-bottom"];   // bun is always there

  addPatty()  { this.parts.push("patty");   return this; }  // return this →
  addCheese() { this.parts.push("cheese");  return this; }  // lets calls chain
  addBacon()  { this.parts.push("bacon");   return this; }

  build(): Burger {                            // finish → hand back the product
    this.parts.push("bun-top");
    return new Burger(this.parts);
  }
}

// Reads like an order: patty, cheese, bacon — done.
const burger = new BurgerBuilder()
  .addPatty()
  .addCheese()
  .addBacon()
  .build();

Notice how you only mention the parts you want. No cheese? Just don't call .addCheese(). Want two patties? Call .addPatty() twice. The order is up to you, and every line says exactly what it does — there is no row of mystery booleans to decode.

"return this" is the whole trick behind the chain

Each step returns the builder itself (return this), so the value of .addPatty() is the builder, ready for .addCheese() to be called on it. Chain them and you get one flowing sentence. This style — methods that return the object so calls link together — is called a fluent interface.

The Director: a helper that runs a known recipe

Sometimes the same combination of steps gets used over and over — a Classic Cheeseburger is always bun, patty, cheese, sauce. Instead of making every caller remember that recipe, you can wrap it in a Director: a small helper whose job is to run a fixed sequence of builder steps for you.

typescript
class BurgerDirector {
  // The Director knows the recipe; the caller doesn't have to.
  classicCheeseburger(b: BurgerBuilder): Burger {
    return b.addPatty().addCheese().addSauce().build();
  }
}

const burger = new BurgerDirector()
  .classicCheeseburger(new BurgerBuilder());   // one call → a preset burger

Now a caller who just wants the standard cheeseburger asks the Director for it and gets a finished burger — without knowing or repeating the individual steps. The Director is optional: you only reach for it when you have a few named presets worth naming. The prototype's two preset buttons (Classic Cheeseburger and Veggie Deluxe) are exactly this idea — one click runs a fixed recipe of builder steps.

Why the constructor way goes wrong

If you build the same burger with a plain constructor, you have to pass a value for every optional part in a fixed position: new Burger(true, true, false, true, false, true, false). Which flag is cheese? Which is bacon? You can't tell without opening the constructor and counting. Add one more topping later and every call site with that long argument list can silently break. The Builder makes each choice named and optional, so the code stays readable no matter how many toppings exist. Flip the prototype's Constructor way ↔ Builder way switch to feel the difference on the very same burger.

Interactive prototype

See it. Build it. Break it.

A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.

About this simulation

Build a burger by clicking ingredient toggles — 🥩 Patty, 🧀 Cheese, 🥓 Bacon, 🥬 Lettuce, 🍅 Tomato, 🧂 Sauce, and 🥩🥩 Double Patty (the bun is always there, top and bottom). Two things update together on every click: a visual burger stack where each layer drops in or out, and a live builder-code panel where the fluent chain grows and shrinks — new BurgerBuilder().addCheese()…build() — always ending in .build(). Flip the ⚙ Constructor way ↔ Builder way switch to see the same burger built with the awful telescoping constructor new Burger(true, false, true, …) where nobody can tell which boolean is cheese, next to the readable builder chain. Two Director buttons — 🍔 Classic Cheeseburger and 🥬 Veggie Deluxe — fill the whole burger in one click by running a fixed recipe of steps. The single explain panel narrates each toggle; nothing scrolls away.

Hands-on

Try these yourself

Open the prototype above, predict what happens, then verify.

try 01

Toggle toppings and watch two views move together

Open the prototype. Click 🧀 Cheese, then 🥓 Bacon. Two things update at once: the burger stack drops a cheese layer and a bacon layer in, and the code panel grows two new lines — .addCheese() and .addBacon() — always sitting above the final .build(). Toggle 🧀 Cheese off again and watch both the layer and the .addCheese() line disappear. This is 'build the object step by step' made visible.

try 02

Flip Constructor way ↔ Builder way

Add a few toppings, then click the ⚙ Constructor way ↔ Builder way switch. The code panel now shows the same burger as one telescoping constructor: new Burger(true, false, true, …) with tiny labels asking 'which boolean is cheese?!'. Flip back to the Builder way and the readable named chain returns. This is the core 'why' of the pattern — same object, wildly different readability.

try 03

Let the Director run a recipe

Click 🍔 Classic Cheeseburger. In one click the toggles, the burger stack, and the code chain all fill in with a fixed recipe — you didn't pick the steps, the Director did. Now click 🥬 Veggie Deluxe and watch it swap to a different preset. That's the Director: a helper that runs a known sequence of builder steps so callers get a preset without knowing the individual steps.

In practice

When to use it — and what trips people up

Reach for a Builder when construction gets messy

The Builder shines whenever making an object with a plain constructor would be confusing, error-prone, or rigid:

  • Many optional fields. When an object has lots of parts that may or may not be present, a Builder lets callers set only the ones they want by name instead of passing a long line of true/false/null.
  • The object should be immutable once built. Gather all the choices in the builder, then produce a finished object that can't be changed afterwards — a very common and safe pattern.
  • Step-by-step or validated assembly. Config objects, HTTP request builders, SQL query builders, StringBuilder, and test-data builders all assemble a result piece by piece, sometimes checking rules before build() hands it back.
  • A few named presets exist. When common combinations repeat (a Classic Cheeseburger), a Director wraps the recipe so callers get the preset in one call.

Don't reach for a Builder when a constructor will do

A Builder is extra code — a whole second class with a method per field. If your object has just two or three required fields and no real optional ones, a plain constructor (or, in Python, keyword arguments / a dataclass) is clearer and shorter. Use the Builder to tame genuinely complex construction, not to dress up a simple object.

What it gives you

  • Readable, self-documenting creation — .addCheese().addBacon() says exactly what each step does, unlike a row of positional booleans.
  • Only set what you want — optional parts are simply omitted, so there's no long list of nulls/defaults to pass through.
  • Order-flexible and hard to misuse — named steps can't be swapped by accident the way positional arguments can.
  • Produces immutable products cleanly — gather choices in the builder, then hand back a finished object nobody can alter.
  • Presets via a Director — common recipes get a single named entry point without callers repeating the steps.

Common mistakes

  • More code to write — you add a separate builder class with a method per field, which is overkill for simple objects.
  • Indirection — construction is split across the product and its builder, so there's one more hop to follow when reading the code.
  • Often unnecessary in some languages — Python's keyword arguments and Kotlin/Scala default+named parameters already give named, optional fields without a builder.
  • Easy to leave the product half-built — if you forget to call .build() (or a required step), you may get a builder or an incomplete object instead of what you wanted.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

// 😖 The telescoping constructor — which boolean is cheese?!
// new Burger(true, false, true, true, false, true, false);

class Burger {
  constructor(readonly parts: string[]) {}   // finished & immutable
  toString() { return this.parts.join(" · "); }
}

class BurgerBuilder {
  private parts: string[] = ["bun-bottom"];  // bun is always present

  addPatty()  { this.parts.push("patty");  return this; }   // return this →
  addCheese() { this.parts.push("cheese"); return this; }   // enables chaining
  addBacon()  { this.parts.push("bacon");  return this; }
  addSauce()  { this.parts.push("sauce");  return this; }

  build(): Burger {                          // hand back the finished product
    this.parts.push("bun-top");
    return new Burger(this.parts);
  }
}

// A Director bundles a fixed recipe of steps into one call (optional).
class BurgerDirector {
  classicCheeseburger(b: BurgerBuilder): Burger {
    return b.addPatty().addCheese().addSauce().build();
  }
}

// Client: named, optional, order-flexible steps → one readable line.
const custom = new BurgerBuilder().addPatty().addBacon().build();
const classic = new BurgerDirector().classicCheeseburger(new BurgerBuilder());
console.log(custom.toString());   // bun-bottom · patty · bacon · bun-top

References & further reading

6 sources

Knowledge check

Did it land?

Quick questions, answers revealed on submit. Sign in to save your best score.

question 01 / 05

What core problem does the Builder pattern solve?

question 02 / 05

In a fluent Builder like new BurgerBuilder().addPatty().addCheese().build(), what does the build() call do?

question 03 / 05

Why can the builder methods be chained into one readable line?

question 04 / 05

What is the role of the Director in the Builder pattern?

question 05 / 05

When is a Builder usually overkill?

0/5 answered