In this post, we will learn how to use the builder pattern in Javascript, and see some code examples, as well as advanced concepts like validation and fixed attribute options.

banner

If you just want to see the example code, you can view it on Github

Basic Usage - Creating a Builder Class

The builder pattern is a popular object oriented design pattern that makes it easier to construct object instances.

For example, consider a Car class with some properties:

  • The color of the car
  • Whether it has a spoiler or not
  • Its fuelType - this should either be “petrol” or “diesel”
  • Its productionDate, which should be a Javascript Date type

Let’s see how we can define it’s class:

class Car {
    // these attributes will be private, so we prefix them with `#`
    #color = null
    #spoiler = null
    #fuelType = null
    #productionDate = null

    constructor(color, spoiler, fuelType, productionDate) {
        this.#color = color
        this.#spoiler = spoiler
        this.#fuelType = fuelType
        this.#productionDate = productionDate
    }

    // we should be able to print information about the car according to its parameters
    toString() {
        return `color: ${this.#color}
spoiler: ${this.#spoiler}
fuel type: ${this.#fuelType}
production date: ${this.#productionDate}`
    }
}

We can now construct an instance and print it to the console:

const car = new Car('red', true, 'petrol', new Date('2020-09-21'))

console.log(car.toString())

This should give the output:

color: red
spoiler: false
fuel type: petrol
production date: Tue Sep 21 2021 05:30:00 GMT+0530

Let’s look at some of the problems with this code that can cause bugs and readability issues:

  1. You need to remember the order of the arguments: Someone trying to construct a new Car may get confused which argument is for color, and which is for the fuelType.
  2. Some attributes, like fuelType, are supposed to have a fixed number of possible values (in this case "petrol" or "diesel") - it is easy to make an error here (what if we misspell "diesel" as "deisel"?)

These problems will only get worse for objects that have more attributes.

A builder class is a special class that helps us specify each attribute in a method, before finally constructing the intended object.

To make a builder for the above example, we can create a static Builder class within the parent Car class:

class Car {

    #color = null
    #spoiler = null
    #fuelType = null
    #productionDate = null

    // We define a static Builder class within `Car`
    static Builder = class {
        // the builder class will have the same attributes as
        // the parent
        #color = null
        #spoiler = null
        #fuelType = null
        #productionDate = null

        // there are four methods to set each of the four
        // attributes
        setColor(color) {
            this.#color = color
            // each method returns the builder object itself
            // this allows for chaining of methods
            return this
        }

        setSpoiler(spoiler) {
            this.#spoiler = spoiler
            return this
        }

        setFuelType(fuelType) {
            this.#fuelType = fuelType
            return this
        }

        setProductionDate(date) {
            this.#productionDate = date
            return this
        }

        // when we're done setting arguments, we can call the build method
        // to give us the `Car` instance
        build() {
            const car = new Car(
                this.#color,
                this.#spoiler,
                this.#fuelType,
                this.#productionDate)
            return car
        }
    }

    constructor(color, spoiler, fuelType, productionDate) {
        this.#color = color
        this.#spoiler = spoiler
        this.#fuelType = fuelType
        this.#productionDate = productionDate
    }

    toString() {
        return `color: ${this.#color}
spoiler: ${this.#spoiler}
fuel type: ${this.#fuelType}
production date: ${this.#productionDate}`
    }

}

We can use the Builder class to construct a Car instance, instead of using the new Car(...) constructor:

const car = new Car.Builder()
    .setColor('red')
    .setFuelType('petrol')
    .setProductionDate(new Date('2021-09-21'))
    .setSpoiler(false)
    .build()

console.log(car.toString())

It’s now much easier to recognize different attributes when they’re being set. We can also change the order of the builder methods being called and get the same result.

Now let’s look at some alternate ways to implement our builder methods to better suit our requirements.

Setting Constant Attribute Values

Some of the attributes, like fuelType only have a fixed number of values. Instead of placing the responsibility on the user of our class, we can create special builder methods to set the correct fuel type:

static Builder = class {
        //...

        // these methods don't take any arguments, and set the 
        // required hard-coded values
        withPetrolFuelType() {
                this.#fuelType = 'petrol'
                return this
        }

        withDieselFuelType() {
                this.#fuelType = 'diesel'
                return this
        }

        // we can also do the same with boolean types
        // since they only have two possible values
        withSpoiler() {
                this.#spoiler = true
                return this
        }

        withOutSpoiler() {
                this.#spoiler = false
                return this
        }

        //...
}

This makes our code less error prone, since the only way to set the fuelType is by choosing from one of the constant options provided:

const car = new Car.Builder()
    .setColor('red')
    // using the constant attribute method
    .withPetrolFuelType()
    .setProductionDate(new Date('2021-09-21'))
    // In this case, the method is semantically more readable
    .withOutSpoiler()
    .build()

Adding Attribute Validation

We can use builder methods to add validation for our attributes.

For our example, productionDate should be a valid Date object. Let’s validate that in the setProductionDate method:

setProductionDate(date) {
        // check if `date` is a `Date` object, and that it
        // is a valid date (isNan should be false)
        if (!(date instanceof Date) || isNaN(date)) {
                // if our conditions fail, throw an error
                throw new Error('invalid production date')
        }
        this.#productionDate = date
        return this
}

As opposed to adding all our validations in the constructor, we can add it to each builder method instead. This helps us separate each validation according to its attribute, and allows us to fail fast incase a validation fails.

Multiple Builder Types

Sometimes, there can be multiple use-cases to build the same type of object. In this case, we can make different builders depending on our use case.

Consider a race car: it’s like a regular car, but the fuelType is always "petrol" and it always has a spoiler.

We can create a new builder class for building race cars, which makes use of our original builder:

class Car {

    // ...

    static RaceCarBuilder = class {
            #color = null
            #productionDate = null

            setColor(color) {
                    this.#color = color
                    return this
            }

            setProductionDate(date) {
                    this.#productionDate = date
                    return this
            }

            // We can use our original builder and fix specific attributes
            // like `fuelType` and `spoiler`
            build() {
                    return new Car.Builder()
                            .setColor(this.#color)
                            .setProductionDate(this.#productionDate)
                            .withPetrolFuelType()
                            .withSpoiler()
                            .build()
            }
    }

    // ...
}

We can now make it easier to construct these kind of cars:

const raceCar = new Car.RaceCarBuilder()
    .setColor('green')
    .setProductionDate(new Date('2021-09-21'))
    .build()

console.log(raceCar.toString())

Note that this is not the same as class inheritance. In this example, there is no separate RaceCar class. We are just defining a different builder to help us construct a Car instance that has some additional constraints.

To see the example code for all the examples discussed here, you can view the repo on Github

When to Use the Builder Pattern

The builder pattern is useful when we want to make the construction of an object easier and more readable.

In classes with more than five attributes, it’s difficult to call the constructor without forgetting the order of arguments.

For objects with private attributes, or with attributes that can only be set at the time of construction, using a builder allows us to set attributes at different points in our code.

However, there are some obvious downsides to using builders as well:

  1. Bloated code: Adding a Builder class within our Car class required us to add a lot more code than if we just used the constructor. Additionally, it can seem repetitive since the builder class required similar attributes to the main class.
  2. We need to construct two objects now: the Builder instance, and the actual Car itself. This is not ideal if your application is sensitive to memory constraints.

In general, it makes sense to add a builder class if your original class is expected to be used multiple times in your application code. It’s also a better tradeoff if there are a lot of attributes that need to be configured to build an object instance.

Can you think of any other uses or pitfalls of using the builder pattern in your Javascript application? Let me know in the comments!