We use cookies to enhance your experience on the site
CodeWorlds

The Builder Pattern in JavaScript

In Jurassic Park, creating a new dinosaur is a complex process. Dr. Henry Wu can't just dump all the parameters into one constructor — genetic sequence, diet, habitat, danger level, temperature requirements... dozens of parameters! Fortunately there's the Builder pattern — an elegant way to construct complex objects step by step.

The Problem: Too Many Parameters

Imagine a dinosaur constructor with many parameters:

1// Constructor with many parameters — unreadable and error-prone
2const dino = new Dinosaur(
3  "Rexy",          // name
4  "Tyrannosaurus",  // species
5  "carnivore",      // diet
6  8,                // age
7  12000,            // weight
8  "Sector A",       // habitat
9  10,               // dangerLevel
10  true,             // isActive
11  38,               // optimalTemp
12  null,             // mate
13  ["hunt", "roar"]  // abilities
14);
15// Which parameter is which? Easy to mix up the order!

The Builder Pattern — Solution

Builder lets you construct an object step by step with a readable API based on method chaining:

1class DinosaurBuilder {
2  constructor(name, species) {
3    // Required parameters in constructor
4    this.dinosaur = {
5      name,
6      species,
7      abilities: []
8    };
9  }
10
11  // Each method sets a property and returns this (the builder)
12  setDiet(diet) {
13    this.dinosaur.diet = diet;
14    return this; // Critical! Returns the builder, enabling chaining
15  }
16
17  setAge(age) {
18    this.dinosaur.age = age;
19    return this;
20  }
21
22  setWeight(weight) {
23    this.dinosaur.weight = weight;
24    return this;
25  }
26
27  setHabitat(habitat) {
28    this.dinosaur.habitat = habitat;
29    return this;
30  }
31
32  setDangerLevel(level) {
33    this.dinosaur.dangerLevel = level;
34    return this;
35  }
36
37  setActive(isActive) {
38    this.dinosaur.isActive = isActive;
39    return this;
40  }
41
42  setOptimalTemperature(temp) {
43    this.dinosaur.optimalTemp = temp;
44    return this;
45  }
46
47  addAbility(ability) {
48    this.dinosaur.abilities.push(ability);
49    return this;
50  }
51
52  // build() creates the final object
53  build() {
54    // Validation before building
55    if (!this.dinosaur.diet) {
56      throw new Error("Diet is required!");
57    }
58    return { ...this.dinosaur };
59  }
60}
61
62// Usage — readable and self-documenting
63const rexy = new DinosaurBuilder("Rexy", "Tyrannosaurus")
64  .setDiet("carnivore")
65  .setAge(8)
66  .setWeight(12000)
67  .setHabitat("Sector A")
68  .setDangerLevel(10)
69  .setActive(true)
70  .setOptimalTemperature(38)
71  .addAbility("hunt")
72  .addAbility("roar")
73  .build();
74
75console.log(rexy);
76// { name: "Rexy", species: "Tyrannosaurus", abilities: ["hunt", "roar"],
77//   diet: "carnivore", age: 8, weight: 12000, habitat: "Sector A",
78//   dangerLevel: 10, isActive: true, optimalTemp: 38 }

Fluent API — Method Chaining in Practice

The key to the Builder pattern is the Fluent API — each method returns

this
, allowing calls to be chained:

1// Fluent API for building dinosaur queries
2class DinoQueryBuilder {
3  constructor() {
4    this.query = {
5      filters: {},
6      sort: null,
7      limit: 100,
8      offset: 0
9    };
10  }
11
12  whereSpecies(species) {
13    this.query.filters.species = species;
14    return this;
15  }
16
17  whereDiet(diet) {
18    this.query.filters.diet = diet;
19    return this;
20  }
21
22  whereDangerAbove(level) {
23    this.query.filters.minDanger = level;
24    return this;
25  }
26
27  whereActive(isActive = true) {
28    this.query.filters.isActive = isActive;
29    return this;
30  }
31
32  sortBy(field, direction = "asc") {
33    this.query.sort = { field, direction };
34    return this;
35  }
36
37  limitTo(count) {
38    this.query.limit = count;
39    return this;
40  }
41
42  skip(count) {
43    this.query.offset = count;
44    return this;
45  }
46
47  build() {
48    return { ...this.query };
49  }
50
51  execute(database) {
52    const query = this.build();
53    let results = [...database];
54
55    if (query.filters.species) results = results.filter(d => d.species === query.filters.species);
56    if (query.filters.diet) results = results.filter(d => d.diet === query.filters.diet);
57    if (query.filters.minDanger) results = results.filter(d => d.dangerLevel >= query.filters.minDanger);
58    if (query.filters.isActive !== undefined) results = results.filter(d => d.isActive === query.filters.isActive);
59
60    if (query.sort) {
61      results.sort((a, b) => {
62        const dir = query.sort.direction === "asc" ? 1 : -1;
63        return a[query.sort.field] > b[query.sort.field] ? dir : -dir;
64      });
65    }
66
67    results = results.slice(query.offset, query.offset + query.limit);
68    return results;
69  }
70}
71
72// Sample database
73const dinoDatabase = [
74  { name: "Rexy", species: "Tyrannosaurus", diet: "carnivore", dangerLevel: 10, isActive: true },
75  { name: "Blue", species: "Velociraptor", diet: "carnivore", dangerLevel: 8, isActive: true },
76  { name: "Spike", species: "Triceratops", diet: "herbivore", dangerLevel: 4, isActive: true },
77  { name: "Echo", species: "Velociraptor", diet: "carnivore", dangerLevel: 7, isActive: false },
78  { name: "Trike", species: "Triceratops", diet: "herbivore", dangerLevel: 3, isActive: true }
79];
80
81// Building a query with Fluent API
82const dangerousCarnivores = new DinoQueryBuilder()
83  .whereDiet("carnivore")
84  .whereDangerAbove(7)
85  .whereActive()
86  .sortBy("dangerLevel", "desc")
87  .limitTo(5)
88  .execute(dinoDatabase);
89
90console.log("Dangerous carnivores:", dangerousCarnivores);
91// [{ name: "Rexy", ... }, { name: "Blue", ... }]

Builder with Validation and Default Values

1class EnclosureBuilder {
2  constructor(name) {
3    this.enclosure = {
4      name,
5      type: "standard",
6      fenceVoltage: 10000,
7      size: 1000,
8      features: [],
9      maxCapacity: 5,
10      currentDinosaurs: []
11    };
12    this.errors = [];
13  }
14
15  setType(type) {
16    const validTypes = ["standard", "aquatic", "aviary", "high-security"];
17    if (!validTypes.includes(type)) {
18      this.errors.push(`Invalid enclosure type: ${type}`);
19    }
20    this.enclosure.type = type;
21    return this;
22  }
23
24  setFenceVoltage(voltage) {
25    if (voltage < 5000) {
26      this.errors.push("Fence voltage must be >= 5000V");
27    }
28    this.enclosure.fenceVoltage = voltage;
29    return this;
30  }
31
32  setSize(squareMeters) {
33    if (squareMeters < 100) {
34      this.errors.push("Minimum area is 100 m²");
35    }
36    this.enclosure.size = squareMeters;
37    return this;
38  }
39
40  setMaxCapacity(capacity) {
41    this.enclosure.maxCapacity = capacity;
42    return this;
43  }
44
45  addFeature(feature) {
46    this.enclosure.features.push(feature);
47    return this;
48  }
49
50  addDinosaur(dinosaur) {
51    if (this.enclosure.currentDinosaurs.length >= this.enclosure.maxCapacity) {
52      this.errors.push(`Enclosure full! Max: ${this.enclosure.maxCapacity}`);
53    }
54    this.enclosure.currentDinosaurs.push(dinosaur);
55    return this;
56  }
57
58  build() {
59    if (this.errors.length > 0) {
60      throw new Error(
61        `Enclosure build errors:\n${this.errors.join("\n")}`
62      );
63    }
64    return Object.freeze({ ...this.enclosure }); // Frozen object
65  }
66}
67
68// Building an enclosure
69const raptorPaddock = new EnclosureBuilder("Raptor Paddock")
70  .setType("high-security")
71  .setFenceVoltage(25000)
72  .setSize(5000)
73  .setMaxCapacity(4)
74  .addFeature("electrified-fence")
75  .addFeature("motion-sensors")
76  .addFeature("reinforced-gates")
77  .addDinosaur("Blue")
78  .addDinosaur("Charlie")
79  .addDinosaur("Delta")
80  .build();
81
82console.log(raptorPaddock);

Functional Builder (Without Classes)

Builder doesn't require classes — we can use simple functions:

1function createDinoReport(name) {
2  const report = { name, sections: [] };
3
4  const builder = {
5    addVitals(heartRate, temperature) {
6      report.sections.push({ type: "vitals", heartRate, temperature });
7      return builder;
8    },
9
10    addBehavior(description, threatLevel) {
11      report.sections.push({ type: "behavior", description, threatLevel });
12      return builder;
13    },
14
15    addNote(text) {
16      report.sections.push({
17        type: "note",
18        text,
19        timestamp: new Date().toISOString()
20      });
21      return builder;
22    },
23
24    build() {
25      return {
26        ...report,
27        generatedAt: new Date().toISOString(),
28        totalSections: report.sections.length
29      };
30    }
31  };
32
33  return builder;
34}
35
36// Building a report
37const report = createDinoReport("Rexy")
38  .addVitals(65, 38.2)
39  .addBehavior("Calm, eating regularly", "low")
40  .addNote("New hunting pattern observed")
41  .addVitals(120, 39.1)
42  .addBehavior("Agitated after the storm", "medium")
43  .build();
44
45console.log(JSON.stringify(report, null, 2));

Summary

Dr. Wu summarizes: "The Builder pattern is like the protocol for creating a dinosaur — step by step, with validation at each stage:"

  1. Builder — constructs complex objects step by step instead of one massive constructor
  2. Fluent API / Method chaining — each method returns
    this
    , enabling chained calls
  3. Validation
    build()
    checks correctness before creating the object
  4. Readability — code is self-documenting, each parameter has a named method

The Builder pattern is especially useful when:

  • An object has many optional parameters
  • You want to validate data before creating the object
  • You're building queries, configurations, or complex data structures
Go to CodeWorlds