Phase 6 · Design patterns · Topic 6.3

Builder

Must know

Builds a complex object step by step, instead of using a constructor with a dozen parameters.

Not started

Step 1 Understand it

The problem it solves. Objects with many optional settings lead to long, confusing constructors, where it's easy to pass arguments in the wrong order.

Watch it happen

AnimationBuilder: set the parts by name, then build once
Read the animation as text
  1. A request has many optional parts. A constructor with all of them in a row — Request(url, method, None, None, 5…) — is unreadable and easy to get wrong.
  2. A builder sets each part with a clearly named method, starting from sensible defaults. Each method returns the builder, so the calls chain.
  3. .method(…) sets one more part.
  4. .header(…) sets one more part.
  5. .body(…) sets one more part.
  6. .timeout(…) sets one more part.
  7. build() checks the combination is valid — the URL is set, the timeout is in range — then returns the finished object, which can be read-only.

Picture it

flowchart LR
  B0["PizzaBuilder('large')"] --> B1[".add('mushroom')"] --> B2[".add('olive')"] --> B3[".with_extra_cheese()"] --> B4[".build()<br/>checks it's valid"] --> P["Pizza ✓"]
Each step sets one part; build() checks everything and returns the finished object.

The idea

When an object has many optional parts, a constructor like Pizza(size, cheese, olives, ham, …) is unreadable and easy to get wrong. A sets each part with a clearly named method, then build() checks everything and returns the finished object.

How it works, step by step

  1. Create a builder with sensible defaults for every setting.
  2. Each method sets one thing and returns the builder, so calls can be chained.
  3. build() checks the combination is valid: required fields set, values in range.
  4. build() returns the finished object, which can then be read-only.

Analogy

Ordering a custom sandwich at a counter: choose the bread, then the fillings, then the sauce, then say 'done'.

Where the analogy breaks

At the counter you can choose in any order. Some objects need steps in a valid order, or have required parts, so build() must check before returning anything.

Example

Building an : Request.builder().url(...).method('POST').header(...).body(...).(5).build().

Must-know points

  • Use it for objects with many optional settings, or multi-step construction.
  • Fluent style: each step returns the builder, so calls can be chained.
  • Validate in build(); the finished object can then be unchangeable.
  • In Python, keyword arguments with defaults often solve the same problem; in Java, Builder is very common.

Common mistakes

  • Letting the half-built object escape before build() is called.
  • Using a builder for objects with only two or three fields.
python
class Pizza:
    def __init__(self, size, toppings, extra_cheese):
        self.size, self.toppings, self.extra_cheese = size, toppings, extra_cheese

class PizzaBuilder:
    def __init__(self, size):
        self.size, self.toppings, self.extra_cheese = size, [], False

    def add(self, topping):
        self.toppings.append(topping)
        return self                     # return self to chain calls

    def with_extra_cheese(self):
        self.extra_cheese = True
        return self

    def build(self):
        if not self.toppings:
            raise ValueError("a pizza needs at least one topping")
        return Pizza(self.size, list(self.toppings), self.extra_cheese)

pizza = PizzaBuilder("large").add("mushroom").add("olive").with_extra_cheese().build()

Step 2 Go further (free, optional)

Optional. The notes above are written to be enough on their own. These free resources give a second explanation and extra practice.

Step 3 Practise

Try each exercise yourself first: on paper, out loud or in code. Then open the answer and compare it with yours.

  1. ExplainSay it in plain words in under a minute, with no notes.

    Explain Builder with the sandwich-counter picture.

    Show answer

    At a sandwich counter you don't shout the whole order in one breath. You build it step by step: "white bread", "chicken", "lettuce", "no mayo", and then "that's it". Each choice is clearly named, you can skip what you don't want, and when you say "done", the person behind the counter checks it makes sense ("you didn't pick a bread") before making it.

    A builder does the same for objects with many optional parts. Instead of Pizza("large", None, True, False, ["olive"], …), where nobody can remember which position means what, you call clearly named , one per part, each returning the builder so they chain: PizzaBuilder("large").add("olive").with_extra_cheese(). Finally build() checks the rules and returns the finished, complete object.

  2. DrawSketch the diagram (or class diagram) from memory.

    Draw PizzaBuilder and Pizza as a .

    Show answer

    The builder holds the parts while they are being chosen, and every setter returns the builder itself, which is what lets the calls chain. Only build() creates the Pizza, after checking the rules, so a half-finished pizza can never escape.

    text
    PizzaBuilder                          Pizza
     - size                                - size
     - toppings: list                      - toppings
     - extra_cheese: bool                  - extra_cheese
     + add(topping): PizzaBuilder ─┐
     + with_extra_cheese():         ├ return self,
         PizzaBuilder ─────────────┘ so calls chain
     + build(): Pizza ── checks the rules,
                         then creates ──►  Pizza
  3. ApplyUse it in a real design question and say exactly where it fits.

    Write an EmailBuilder (to, cc, subject, body, attachments) whose build() fails if there's no recipient.

    Show answer

    Each part has its own named method that returns self, so the calls chain and read like a sentence. to(), cc() and attach() can be called several times. build() enforces the one hard rule, at least one recipient, and hands back copies of the lists, so changing the builder afterwards can't change an email already built.

    python
    class Email:
        def __init__(self, to, cc, subject, body, attachments):
            self.to, self.cc, self.subject = to, cc, subject
            self.body, self.attachments = body, attachments
    
    
    class EmailBuilder:
        def __init__(self):
            self._to, self._cc, self._attachments = [], [], []
            self._subject = self._body = ""
    
        def to(self, *addresses):
            self._to.extend(addresses)
            return self
    
        def cc(self, *addresses):
            self._cc.extend(addresses)
            return self
    
        def subject(self, text):
            self._subject = text
            return self
    
        def body(self, text):
            self._body = text
            return self
    
        def attach(self, filename):
            self._attachments.append(filename)
            return self
    
        def build(self):
            if not self._to:
                raise ValueError("an email needs at least one recipient")
            return Email(list(self._to), list(self._cc), self._subject, self._body, list(self._attachments))
    
    
    email = (EmailBuilder()
             .to("priya@mail.com")
             .cc("tom@mail.com")
             .subject("Invoice 105")
             .body("Your invoice is attached.")
             .attach("invoice-105.pdf")
             .build())
    print(email.to, email.attachments)            # ['priya@mail.com'] ['invoice-105.pdf']
    
    try:
        EmailBuilder().subject("Nobody to send to").build()
    except ValueError as error:
        print(error)                              # an email needs at least one recipient
  4. Trade-offsSay what it costs, when not to use it, and the alternative.

    Builder or keyword arguments: when is each the better choice?

    Show answer

    In Python, keyword arguments with defaults (or a dataclass) already solve the main problem the pattern was invented for in Java: Pizza(size="large", toppings=["olive"], extra_cheese=True) is readable, needs no extra , and a __post_init__ can check the rules. That is the better choice most of the time.

    A builder is the better choice when:
    • the object is assembled step by step, across several places or in a loop ("add a header if…", "attach each file"), not in one call;
    • validation spans several parts and should run once, at the end (build());
    • the result should be immutable, but getting there takes many steps;
    • you want a fluent, readable language for complex things: query builders, HTTP request builders, test-data builders;
    • the language has no named or default arguments (Java, older C++).

Step 4 Interview-ready, then prove it

Your interview answer

About 20 to 40 seconds, said out loud. Practise it until it sounds like you, not like notes.

Builder constructs a complex object step by step through readable, named methods, then validates it and returns it, instead of a constructor with many parameters. I'd use it for things like requests or reports with lots of optional settings.

Follow-up questions

Why is Builder less needed in Python?

Keyword arguments with defaults already make optional settings readable. Builder still helps when construction has several steps or needs validation.

Tick each check when you can do it without notes

A topic is finished when you pass all four checks, not when you finish a video. Be strict: if you needed your notes, it is not ticked yet.

  • Explain Say it in plain words in under a minute, with no notes.
  • Draw Sketch the diagram (or class diagram) from memory.
  • Apply Use it in a real design question and say exactly where it fits.
  • Trade-offs Say what it costs, when not to use it, and the alternative.

0 of 4 checks ticked.

Saved in this browser. Back it up or move it to another device.