RizTech Academy logo
RizTech Academy
Design Patterns in JavaScriptLesson 3 of 630 min

Factories, and why JavaScript rarely needs a builder

A factory is any function that returns an object. That is a low bar, and in JavaScript it is met constantly — which is why this lesson is mostly about when the extra machinery is not needed.

The factory function

function createOrder(customer, item, pieces) {
  return {
    id: crypto.randomUUID(),
    customer,
    item,
    pieces,
    placedAt: new Date().toISOString(),
    delivered: false,
  };
}
const order = createOrder("Asha Kulkarni", "veg", 2);

Three things it gives you that a bare object literal does not:

A name. createOrder(...) says what is being made. An object literal at the call site says only what shape it has.

Defaults and derived fields in one place. The id, the timestamp and delivered: false are set once here rather than at every call site, where one of them will eventually be forgotten.

Freedom to change what comes back. Callers depend on the function, not on a constructor, so you can return a different shape — or a cached instance, or a frozen object — without touching them.

Factory or class?

Both are fine. The differences that actually matter:

// factory
function createCounter() {
  let count = 0;                         // genuinely private
  return { increment: () => ++count, value: () => count };
}

// class
class Counter {
  #count = 0;                            // also genuinely private
  increment() { return ++this.#count; }
  value() { return this.#count; }
}
Factory Class
Private state closure #field
this never needed needed, and can be lost
Memory for many instances a copy of each method per object methods shared on the prototype
instanceof no yes
Works with new by mistake harmlessly n/a

The this point is the practical one. A factory's methods close over variables, so they cannot lose their binding:

const counter = createCounter();
button.addEventListener("click", counter.increment);   // works

const c = new Counter();
button.addEventListener("click", c.increment);         // `this` is undefined
button.addEventListener("click", () => c.increment()); // the usual fix

That is the commonest this bug in the language, and factories simply do not have it.

The memory point matters only at scale. Ten thousand objects from a factory each carry their own copies of every method; from a class they share one prototype. For a few dozen objects it is irrelevant; for a list of ten thousand rows it is measurable.

Use a class when you want instanceof, inheritance, or many instances. Use a factory otherwise — and for most application code, "otherwise" is the answer.

JavaScript rarely needs a builder

Java needs builders because a constructor is positional and a call with eight arguments is unreadable. JavaScript has object destructuring, so the problem mostly does not arise:

// unreadable
createOrder("Asha", "veg", 2, "412207", true, false, 3);

// an options object fixes it at the call site
function createOrder({
  customer,
  item,
  pieces = 1,
  pincode = "411001",
  express = false,
}) {
  if (!customer) throw new Error("customer is required");
  return { customer, item, pieces, pincode, express, id: crypto.randomUUID() };
}

createOrder({ customer: "Asha", item: "veg", pieces: 2, pincode: "412207" });

Every value is labelled, optional ones have defaults, order does not matter, and adding a field breaks nobody. That is what a builder is for — achieved with one parameter and no extra class.

So the builder pattern is largely absent from idiomatic JavaScript, and a OrderBuilder class with chainable setters is usually a sign of somebody missing their previous language.

Two places a chainable API still earns its place:

// A query being assembled across several branches
const q = query("orders").where("pincode", "412207").orderBy("placedAt").limit(20);

when the object is built up conditionally across code that does not all run — and when the chain reads as a sentence, which is why fetch, d3 and query builders use it. If you can write the whole thing in one object literal, use the object literal.

Frozen objects

function createRateCard(rates) {
  return Object.freeze({ ...rates });
}

Object.freeze prevents adding, removing or changing properties — silently in sloppy mode, and with a TypeError in a module (modules are always strict).

It is shallow, which catches people:

const card = Object.freeze({ veg: { paise: 8000 } });
card.veg = {};              // ignored or throws — good
card.veg.paise = 1;         // allowed — the nested object is not frozen

Freeze what you hand out, and prefer flat data or structuredClone when the shape is nested.

Abstract factory, briefly

The pattern books give a lot of space to abstract factory — a factory that returns a family of related objects so you can swap the family at once.

In JavaScript that is a module:

// storage/local.js and storage/remote.js export the same functions
const storage = navigator.onLine
  ? await import("./storage/remote.js")
  : await import("./storage/local.js");

A dynamic import() returning one of several modules with the same exports is an abstract factory with no classes involved. You will read about the pattern; you will rarely write one by that name.

Check your work

What a factory gives you over an object literal: a name, defaults and derived fields in one place, and freedom to change what is returned.

Why factories avoid the this bug: their methods close over variables rather than depending on a receiver.

When a class is better: instanceof, inheritance, or many instances sharing methods on the prototype.

Why JavaScript rarely needs a builder: an options object with destructuring and defaults does the same job in one parameter.

When a chainable API still earns its place: an object assembled conditionally across branches, where the chain reads as a sentence.

What Object.freeze does and does not do: prevents changes to the object, shallowly — nested objects stay mutable.

What abstract factory looks like here: a dynamic import() choosing between modules with the same exports.

Practice

  1. Write createOrder as a factory and call it three times. Confirm each has a distinct id.
  2. Rewrite it as a class. List what changed at the call sites.
  3. Pass counter.increment directly to addEventListener from both a factory and a class instance. Explain the difference.
  4. Create 100,000 objects from a factory and from a class. Compare memory in dev tools.
  5. Convert a positional function of yours to an options object with defaults.
  6. Call it with the properties in a different order and confirm it works.
  7. Freeze an object with a nested object inside and try to change both. Do it once in a module and once in a <script> without "use strict".
  8. Write two modules exporting the same functions and choose between them with a dynamic import().
  9. Write an OrderBuilder class with chainable setters, then write the equivalent options object. Decide which you would send for review.
  10. Find a factory function in your capstone. If there is none, find where one would help.

Next: strategy, when a function is already the pattern.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship