Card 03/ 09

CodeDifficulty: Intermediate4 min

Builder Template in Python, Java and TypeScript

Set one part at a time, then call build() to get a finished, valid object.

The Builder template is the pattern in its pure form, with the participant names from the class diagram: a product with one required part and two optional ones, a builder that refuses a product without the required part, and a director with two fixed configurations. This card builds it one participant at a time in Python, and the tabs then give the complete program in Python, Java and TypeScript.

Building the template one participant at a time

Product

A frozen dataclass, so a product cannot change once it exists.

py
@dataclass(frozen=True)
class Product:
    part_a: str
    part_b: Optional[str]
    part_c: Optional[str]

Builder steps

Each step stores one part and returns the builder, which is what lets calls chain.

py
def set_part_a(self, value: str) -> "ConcreteBuilder":
    self._a = value
    return self

build()

The one place that checks the parts and creates the product.

py
def build(self) -> Product:
    if self._a is None:
        raise ValueError("part A is required")
    return Product(self._a, self._b, self._c)

Director

Optional: named configurations, written once.

py
class Director:
    def make_minimal(self, builder: Builder) -> Product:
        return builder.set_part_a("A").build()

The complete template

py
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional


@dataclass(frozen=True)
class Product:
    part_a: str
    part_b: Optional[str]
    part_c: Optional[str]


class Builder(ABC):
    @abstractmethod
    def set_part_a(self, value: str) -> "Builder": ...

    @abstractmethod
    def set_part_b(self, value: str) -> "Builder": ...

    @abstractmethod
    def set_part_c(self, value: str) -> "Builder": ...

    @abstractmethod
    def build(self) -> Product: ...


class ConcreteBuilder(Builder):
    def __init__(self) -> None:
        self._a: Optional[str] = None
        self._b: Optional[str] = None
        self._c: Optional[str] = None

    def set_part_a(self, value: str) -> "ConcreteBuilder":
        self._a = value
        return self

    def set_part_b(self, value: str) -> "ConcreteBuilder":
        self._b = value
        return self

    def set_part_c(self, value: str) -> "ConcreteBuilder":
        self._c = value
        return self

    def build(self) -> Product:
        if self._a is None:
            raise ValueError("part A is required")
        return Product(self._a, self._b, self._c)


class Director:
    def make_minimal(self, builder: Builder) -> Product:
        return builder.set_part_a("A").build()

    def make_full(self, builder: Builder) -> Product:
        return builder.set_part_a("A").set_part_b("B").set_part_c("C").build()


print(ConcreteBuilder().set_part_a("A").set_part_c("C").build())
print(Director().make_minimal(ConcreteBuilder()))
print(Director().make_full(ConcreteBuilder()))
try:
    ConcreteBuilder().set_part_b("B").build()
except ValueError as error:
    print("refused:", error)
OutputRan with python:3.12-slim (Python 3.12.13)
Product(part_a='A', part_b=None, part_c='C')
Product(part_a='A', part_b=None, part_c=None)
Product(part_a='A', part_b='B', part_c='C')
refused: part A is required
Each step returns `self`, and `build()` refuses a product without part A.
java
public class BdTemplate {
    public static void main(String[] args) {
        System.out.println(new ConcreteBuilder().setPartA("A").setPartC("C").build());
        System.out.println(new Director().makeMinimal(new ConcreteBuilder()));
        System.out.println(new Director().makeFull(new ConcreteBuilder()));
        try {
            new ConcreteBuilder().setPartB("B").build();
        } catch (IllegalStateException error) {
            System.out.println("refused: " + error.getMessage());
        }
    }
}

record Product(String partA, String partB, String partC) {}

interface Builder {
    Builder setPartA(String value);
    Builder setPartB(String value);
    Builder setPartC(String value);
    Product build();
}

class ConcreteBuilder implements Builder {
    private String a;
    private String b;
    private String c;

    public ConcreteBuilder setPartA(String value) { a = value; return this; }
    public ConcreteBuilder setPartB(String value) { b = value; return this; }
    public ConcreteBuilder setPartC(String value) { c = value; return this; }

    public Product build() {
        if (a == null) throw new IllegalStateException("part A is required");
        return new Product(a, b, c);
    }
}

class Director {
    Product makeMinimal(Builder builder) {
        return builder.setPartA("A").build();
    }

    Product makeFull(Builder builder) {
        return builder.setPartA("A").setPartB("B").setPartC("C").build();
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
Product[partA=A, partB=null, partC=C]
Product[partA=A, partB=null, partC=null]
Product[partA=A, partB=B, partC=C]
refused: part A is required
A `record` is Java's immutable product, and each step returns `this`.
ts
interface Product {
  readonly partA: string;
  readonly partB?: string;
  readonly partC?: string;
}

interface Builder {
  setPartA(value: string): Builder;
  setPartB(value: string): Builder;
  setPartC(value: string): Builder;
  build(): Product;
}

class ConcreteBuilder implements Builder {
  private a?: string;
  private b?: string;
  private c?: string;

  setPartA(value: string): this { this.a = value; return this; }
  setPartB(value: string): this { this.b = value; return this; }
  setPartC(value: string): this { this.c = value; return this; }

  build(): Product {
    if (this.a === undefined) throw new Error("part A is required");
    return Object.freeze({ partA: this.a, partB: this.b, partC: this.c });
  }
}

class Director {
  makeMinimal(builder: Builder): Product {
    return builder.setPartA("A").build();
  }

  makeFull(builder: Builder): Product {
    return builder.setPartA("A").setPartB("B").setPartC("C").build();
  }
}

console.log(new ConcreteBuilder().setPartA("A").setPartC("C").build());
console.log(new Director().makeMinimal(new ConcreteBuilder()));
console.log(new Director().makeFull(new ConcreteBuilder()));
try {
  new ConcreteBuilder().setPartB("B").build();
} catch (error) {
  console.log("refused:", (error as Error).message);
}
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
{ partA: 'A', partB: undefined, partC: 'C' }
{ partA: 'A', partB: undefined, partC: undefined }
{ partA: 'A', partB: 'B', partC: 'C' }
refused: part A is required
Steps return `this`, and `Object.freeze` makes the product read-only at run time.

The first three lines of output are products built three ways: by a chain that skips part B, and by each of the director's configurations. The last is a builder that was given part B alone, and build() refused it. Validation lives in one method, so no product missing part A can exist anywhere in the program.

Returning the builder from every step is not required by the pattern; it is what makes the chained form possible. A builder whose steps return nothing still works, one statement per part.