cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Creational patterns

Learn factory functions, singletons, builders, modules, revealing modules, and cloning choices for production JavaScript architecture.

By the end, you can
  • 01
    Choose the right creation shapeExplain when a factory, singleton, builder, module, clone, or copy constructor makes object creation clearer.
  • 02
    Trace real JavaScript implementationsStep through closure-backed factories, lazy singletons, and fluent builders without relying on folklore.
  • 03
    Avoid pattern damageSpot hidden global state, shallow copies, this pitfalls, and over-engineered pattern names before they hurt tests.

Patterns are shared vocabulary

A design pattern is a name for a recurring design shape. It helps a team say “use a factory here” or “this singleton is making tests hard” without explaining the whole trade-off every time. It is vocabulary, not a law.

The famous Gang of Four book was written for C++ and Smalltalk. JavaScript has first-class functions, closures, object literals, prototypes, and ES modules, so some classic patterns become tiny. A module singleton can be one exported object. A private object can be a factory with a closure. A builder might be unnecessary when an options object with defaults is enough.

Definition

Creational patterns name ways to create, configure, share, and copy objects. In this lesson you will build factory functions, singletons, builders, module patterns, and clone/copy strategies with real JavaScript functions.

Real-life analogyA workshop with labeled stations

Think of a workshop. The order desk sends each job to the right station, the tool cabinet is shared, the assembly line performs steps in order, and the stencil copies a shape. The labels help you choose where a job belongs.

In real life: Order desk chooses a station
In JavaScript: A factory chooses an implementation
In real life: One shared tool cabinet
In JavaScript: A singleton is one shared instance
In real life: Assembly line checks each step
In JavaScript: A builder validates in build()
In real life: A stencil copies a shape
In JavaScript: Clone and copy rules decide what survives

Where the analogy stops: A workshop has physical tools that cannot be imported twice. JavaScript modules are evaluated once by the module system, while factory calls can create as many fresh objects as you need.

Creation shapes that are easy to confuse
ShapeHow it createsGood fitWatch out
Factory functionA normal function returns a ready-to-use object.Variants, private closure state, avoiding new and this mistakes.Every call may create a new object and new methods unless you share prototypes deliberately.
Classnew calls a constructor and methods usually live on the prototype.Shared methods, instanceof, inheritance, and public class APIs.Forgetting new, binding methods incorrectly, or overusing inheritance.
Constructor functionA pre-class pattern: new Fn() assigns to this, with methods on Fn.prototype.Reading legacy code and understanding what classes compile to conceptually.Easy to call without new and accidentally write to the wrong this in sloppy code.
SingletonOne shared instance is reused by everyone.Config, loggers, feature flags, and process-level adapters.Hidden global state makes tests order-dependent unless callers can inject a fake.
BuilderA fluent object collects choices, then build() validates and returns the result.Queries, test data builders, complex request objects, and readable setup.Too much ceremony when an options object with defaults is enough.

You will use ideas from object basics, constructors and new, class basics, closures, closure patterns, Object.create, private members, why modules exist, import and export, and immutability.

Factory functions

CHANGE INPUTS

A factory function is a plain function that returns an object. It can hide private state in a closure, return different implementations from the same call shape, and avoid the two common constructor mistakes: forgetting new and losing this.

A factory gives each counter private statePop out in the code editor (opens in a new tab)JavaScript
function createCounter(label) {  let count = 0;  return {    label,    next() {      count += 1;      return label + ": " + count;    },    reset() {      count = 0;    },  };} const first = createCounter("factory");const second = createCounter("factory");console.log(first.next());console.log(first.next());console.log(second.next());console.log(first === second);

Read it line by line. Line 1 declares an ordinary function. Line 2 creates a fresh count for that one call. Lines 5 through 8 return a method that can still reach that private variable. Lines 15 and 16 call the factory twice, so the final identity check is false even though both counters use the same label.

Factory, class, and constructor function side by sidePop out in the code editor (opens in a new tab)JavaScript
function createUser(name) {  return {    name,    greet() {      return "Hello, " + name;    },  };} class ClassUser {  constructor(name) {    this.name = name;  }  greet() {    return "Hello, " + this.name;  }} function ConstructorUser(name) {  this.name = name;}ConstructorUser.prototype.greet = function () {  return "Hello, " + this.name;}; console.log(createUser("Ada").greet());console.log(new ClassUser("Lin").greet());console.log(new ConstructorUser("Grace").greet());

All three snippets create objects. The factory closes over name. The class uses new and stores name on this. The constructor function is the older form: new ConstructorUser() sets up this and the prototype method.

Factory choice playground
Notifier factoryPop out in the code editor (opens in a new tab)JavaScript
function createNotifier(channel) {  if (channel === "email") {    return {      send(message) {        return "email: " + message;      },    };  }  if (channel === "sms") {    return {      send(message) {        return "sms: " + message;      },    };  }  return {    send(message) {      return "toast: " + message;    },  };} console.log(createNotifier("email").send("Build passed"));console.log(createNotifier("sms").send("Deploy now"));
Selected resultemail
Factory callcreateNotifier("email")
send(message)email: Build passed
Try it yourself
Choose the implementation

createNotifier(email) returns an object with the same send method shape, but the behavior now prints email: Build passed.

A factory can choose one of several compatible implementations. The caller receives an object with the same public shape.

The notifier is a factory that chooses among implementations. A larger “abstract factory” uses the same idea to create a whole family of related objects: for example, one UI kit factory could return matching createButton and createDialog functions. That broader architecture belongs with later structural lessons; here the key is simple: one creation function can hide the branch and return a compatible public shape.

Singletons

STEP THROUGH

A singleton is one shared instance. JavaScript often does not need a formal class for this: an ES module is evaluated once, so a module-level object exported from one file is shared by importers. A lazy singleton delays creation until someone asks for it.

Lazy singleton replay
Step 0 of 13Ready
Your turn: follow the blue line

A lazy singleton creates one shared object on the first call, then returns the same object every time after that. Step through both calls and watch the second argument get ignored.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function getConfig(env) {  if (!configInstance) {    configInstance = Object.freeze({      env,      apiBase: "https://" + env + ".example.com",      retries: 2,    });  }  return configInstance;} const first = getConfig("prod");const second = getConfig("staging");console.log(first === second);console.log(second.env);console.log(Object.isFrozen(first));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Factory instances vs a singleton
Identity comparison sourcePop out in the code editor (opens in a new tab)JavaScript
function createCounter(label) {  let count = 0;  return {    next() {      count += 1;      return label + ": " + count;    },  };} let configInstance;function getConfig(env) {  if (!configInstance) configInstance = { env };  return configInstance;} const firstCounter = createCounter("box");const secondCounter = createCounter("box");const firstConfig = getConfig("prod");const secondConfig = getConfig("staging");console.log(firstCounter === secondCounter);console.log(firstCounter.next());console.log(secondCounter.next());console.log(firstConfig === secondConfig);console.log(secondConfig.env);
Identity factsfactory
firstCounter === secondCounterfalse
firstCounter.next()box: 1
secondCounter.next()box: 1
Try it yourself

Two factory calls produce two different objects. Their first next() calls both start at 1 because each closure owns a separate count.

The code is intentionally small: identity checks reveal whether creation returns fresh objects or a shared instance.
Hidden state is the real cost

Singletons are fine for read-only config, a process logger, or a feature-flag client. They hurt when tests need different state, because the dependency is hidden behind an import or a getter. Passing a logger or config into a function is dependency injection, the same idea used in mocks and fakes.

Object.freeze can protect the top-level properties of a config object. It does not make the design testable by itself, and it is shallow unless nested objects are frozen or copied too. Prefer a singleton only when sharing is the point.

Builders

STEP THROUGH

A builder is useful when construction has steps. Each method records one choice and returns this or a new builder. The final build() method validates the collected state and returns the finished value.

Builder chain replay
Step 0 of 12Ready
Your turn: follow the blue line

A builder collects choices over several method calls, then validates and returns the finished object or string in build().

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const state = {    table,    fields: ["*"],    filters: [],    limit: null,  };  return {    select(...fields) {      state.fields = fields;      return this;    },    where(field, value) {      state.filters.push({ field, value });      return this;    },    limit(count) {      state.limit = count;      return this;    },    build() {      if (!state.table) throw new Error("table is required");      if (state.limit !== null && state.limit <= 0) {        throw new Error("limit must be positive");      }      const fields = state.fields.join(", ");      const where = state.filters.length        ? " WHERE " + state.filters.map((filter) =>            filter.field + " = " + JSON.stringify(filter.value)          ).join(" AND ")        : "";      const limit = state.limit === null ? "" : " LIMIT " + state.limit;      return "SELECT " + fields + " FROM " + state.table + where + limit;    },  };} const query = createQueryBuilder("users")  .select("id", "email")  .where("active", true)  .limit(2)  .build();console.log(query);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

This query builder is intentionally small, but it shows the real shape. select, where, and limit collect state. build() rejects a missing table or invalid limit before returning a string. A production query builder would also escape identifiers, use parameters, and avoid hand-written SQL strings.

Options object with defaults: often enoughPop out in the code editor (opens in a new tab)JavaScript
function createApiClient({  baseUrl = "/api",  timeout = 3000,  headers = {},} = {}) {  return { baseUrl, timeout, headers };} const fastClient = createApiClient({ timeout: 1000 });console.log(fastClient.baseUrl);console.log(fastClient.timeout);

Many JavaScript APIs should stop here. A named options object with defaults is lightweight, works well with parameters and destructuring, and avoids a fluent object when there are no real steps to validate.

Module and revealing module

HISTORICAL

Before ES modules were everywhere, JavaScript code often used an IIFE: an immediately invoked function expression. The wrapper created private variables, and the returned object revealed only the public functions.

IIFE revealing modulePop out in the code editor (opens in a new tab)JavaScript
const cartModule = (() => {  const items = [];  function add(name, price) {    items.push({ name, price });  }  function total() {    return items.reduce((sum, item) => sum + item.price, 0);  }  return { add, total };})(); cartModule.add("Book", 12);cartModule.add("Pen", 3);console.log(cartModule.total());console.log(cartModule.items);

This is a revealing module because add and total are private function declarations first, then the returned object exposes references to them. The items array is private because it is never returned.

The same privacy is natural in an ES moduleJavaScript
let currentUser = null; export function setCurrentUser(user) {  currentUser = user;} export function getCurrentUser() {  return currentUser;}

Modern modules make this the default: top-level bindings are private to the file unless you export them. Because ES modules are evaluated once per module graph, a module-level object is also the usual JavaScript singleton. Review why modules exist and import and export when that model feels fuzzy.

Cloning and copying

REAL COPIES

The prototype pattern creates new objects from an existing prototype. Copying asks a more precise question: what should survive in the copy? Own properties, nested data, class methods, private fields, or the prototype chain?

Prototype object with a clone methodPop out in the code editor (opens in a new tab)JavaScript
const widgetPrototype = {  describe() {    return this.kind + ":" + this.theme;  },  clone(overrides = {}) {    return Object.assign(      Object.create(Object.getPrototypeOf(this)),      this,      overrides    );  },}; const primary = Object.assign(Object.create(widgetPrototype), {  kind: "button",  theme: "primary",});const danger = primary.clone({ theme: "danger" });console.log(primary.describe());console.log(danger.describe());console.log(Object.getPrototypeOf(danger) === widgetPrototype);

The clone() method uses Object.create(Object.getPrototypeOf(this)) so the copy has the same prototype as the original. Object.assign copies own properties and applies overrides.

clone() and a copy constructorPop out in the code editor (opens in a new tab)JavaScript
class Point {  constructor(xOrPoint, y) {    if (typeof xOrPoint === "object") {      this.x = xOrPoint.x;      this.y = xOrPoint.y;    } else {      this.x = xOrPoint;      this.y = y;    }  }  clone() {    return new Point(this.x, this.y);  }  toString() {    return this.x + "," + this.y;  }} const original = new Point(2, 3);const cloned = original.clone();const copied = new Point(original);console.log(cloned.toString());console.log(copied.toString());console.log(copied instanceof Point);

A clone() method is called on the old object. A copy constructor is called as new Point(other). Both can be shallow or deep; the object or class decides the rule.

Spread is shallowPop out in the code editor (opens in a new tab)JavaScript
const original = {  user: { name: "Ada" },  tags: ["js"],};const spread = { ...original };spread.user.name = "Lin";spread.tags = [...spread.tags, "patterns"];console.log(original.user.name);console.log(original.tags.join(","));console.log(spread.tags.join(","));

Spreading copies top-level properties. The nested user object is still shared, so changing spread.user.name also changes original.user.name. Replacing spread.tags with a new array does not mutate the original array.

structuredClone copies data, not class identityPop out in the code editor (opens in a new tab)JavaScript
class Point {  constructor(x, y) {    this.x = x;    this.y = y;  }  distance() {    return Math.hypot(this.x, this.y);  }} const point = new Point(3, 4);const copied = structuredClone(point);console.log(copied instanceof Point);console.log(typeof copied.distance);console.log(JSON.stringify(copied));

structuredClone is useful for supported data such as arrays, plain objects, dates, maps, and sets. It does not preserve custom class prototypes. The copied point has x and y, but copied instanceof Point is false and copied.distance is undefined.

Copy strategies and what they preserve
StrategyDepthBest use
Spread copyShallowPlain objects and arrays when nested objects can stay shared or are replaced deliberately.
Object.create(proto)Prototype-preservingPrototype pattern objects that want a clone() method and shared behavior.
clone() methodClass or object decidesThe object owns its own copy rules, including private or derived fields.
Copy constructorClass decidesnew Point(other) is explicit and keeps the prototype because the constructor creates a real instance.
structuredCloneDeep for supported dataDates, Maps, Sets, arrays, and plain data; class instances come back as plain objects.

Where these appear in real projects

SORT IT

Production code rarely announces “I am using a pattern.” It says createApiClient, logger, queryBuilder, aUser(), or clone(). The pattern helps you discuss the shape and trade-off.

Practical creational pattern choices
Use caseLikely shapeWhy
API clientsFactory or options objectcreateApiClient({ baseUrl, token }) keeps environment details outside call sites.
Runtime configModule-level singletonA frozen config object is fine when it is read-only and created once at startup.
Query buildersBuilderStep-by-step filters are readable, and build() can validate before sending SQL or URL params.
Test dataBuilder or factoryaUser().withRole("admin").build() keeps tests expressive while still returning plain objects.
UI pluginsAbstract factory ideaOne factory can return a family of related objects, such as matching modal and button implementations.
Which creational pattern fits?
  • createNotifier(channel) returns an email, SMS, or toast sender.
  • getConfig() returns the same frozen object after the first call.
  • `query.select("id").where("active", true).build()`
  • `const p2 = new Point(p1);`
  • `createCounter("cart")` returns an object with private `count`.
  • `userBuilder().withRole("admin").build()`
  • `const copy = structuredClone(settings);`
  • `export const logger = createLogger();`
Try it yourself
0 of 8 correct

Sort each card by the creation problem it solves. Look for fresh objects, shared instances, fluent building, or copying.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common misconceptions

  • “Patterns are mandatory architecture.” Patterns are names. Use the smallest JavaScript shape that solves the problem.
  • “Factories are just classes without class.” Factories can choose implementations and use closure privacy; classes emphasize prototypes and instances.
  • “A singleton is safe if it is frozen.” Freezing protects top-level writes; it does not remove hidden global state or reset tests.
  • “Every chain is a builder.” A builder collects construction state and has a meaningful build(). Some chains are just fluent helpers.
  • “structuredClone is the perfect clone.” It is deep for supported data, but class instances lose their custom prototype.
Misconceptions and the quick check
MisconceptionQuick checkBetter wording
Factory vs class is only syntaxDoes creation choose an implementation or need closure privacy?Factories and classes optimize for different call sites.
Singleton equals global variableIs sharing intentional, read-only, and easy to replace in tests?A singleton is a controlled shared instance, but still shared state.
Builder is always cleanerWould an options object with defaults say the same thing?Use builders for real steps and validation.
Deep copy keeps class methodsDoes the copy still pass instanceof and have prototype methods?Deep data copy and instance copy are different promises.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upPredict two factory counters

Type the three printed values in order.

Starter codePop out in the code editor (opens in a new tab)JavaScript
function makeBox(label) {
  let value = 0;
  return {
    add() {
      value += 1;
      return label + ":" + value;
    },
  };
}
const first = makeBox("cart");
const second = makeBox("cart");
first.add();
console.log(first.add());
console.log(second.add());
console.log(first === second);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFollow a factory branch

    Read the branch and type the exact text printed.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function createNotifier(channel) {
      return channel === "sms"
        ? { send: (message) => "sms: " + message }
        : { send: (message) => "email: " + message };
    }
    console.log(createNotifier("sms").send("Ready"));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeBuild a URL

      Type the URL printed by the builder.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      function createUrlBuilder(path) {
        const params = [];
        return {
          param(name, value) {
            params.push(name + "=" + encodeURIComponent(value));
            return this;
          },
          build() {
            return path + "?" + params.join("&");
          },
        };
      }
      console.log(createUrlBuilder("/users").param("role", "admin user").build());

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpot the shallow spread

        Type the three printed values in order.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const original = { nested: { count: 1 }, tags: ["a"] };
        const copy = { ...original };
        copy.nested.count += 1;
        copy.tags = [...copy.tags, "b"];
        console.log(original.nested.count);
        console.log(original.tags.join(""));
        console.log(copy.tags.join(""));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeChallenge: structuredClone and prototypes

          Type the two values printed by the clone check.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          class Widget {
            constructor(name) {
              this.name = name;
            }
            label() {
              return "widget:" + this.name;
            }
          }
          const copied = structuredClone(new Widget("menu"));
          console.log(copied instanceof Widget);
          console.log(typeof copied.label);

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7What are design patterns in this lesson?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does the factory counter print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function makeCounter() {
                let count = 0;
                return { next: () => ++count };
              }
              const a = makeCounter();
              const b = makeCounter();
              console.log(a.next());
              console.log(b.next());
              console.log(a === b);

              Choose an answer to see the explanation.

            3. Question 3 of 7What does the lazy singleton identity check print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              let instance;
              function getLogger(name) {
                if (!instance) instance = { name };
                return instance;
              }
              const first = getLogger("app");
              const second = getLogger("test");
              console.log(first === second);
              console.log(second.name);

              Choose an answer to see the explanation.

            4. Question 4 of 7What does this builder print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function makeBuilder() {
                const parts = [];
                return {
                  add(value) { parts.push(value); return this; },
                  build() { return parts.join("/"); },
                };
              }
              console.log(makeBuilder().add("api").add("users").build());

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the revealing module hide?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const module = (() => {
                const secret = "inside";
                function reveal() { return secret; }
                return { reveal };
              })();
              console.log(module.reveal());
              console.log(module.secret);

              Choose an answer to see the explanation.

            6. Question 6 of 7What happens when structuredClone copies a class instance?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              class Point {
                constructor(x) { this.x = x; }
                double() { return this.x * 2; }
              }
              const copy = structuredClone(new Point(4));
              console.log(copy instanceof Point);
              console.log(typeof copy.double);

              Choose an answer to see the explanation.

            7. Question 7 of 7Which creation shape is usually the lightweight alternative to a builder?

              Choose an answer to see the explanation.

            Key takeaways

            • Patterns are vocabulary for trade-offs, not rules to force into code.
            • Factories return fresh objects and can use closures or choose implementations.
            • Singletons are shared instances; use them for intentional sharing and inject dependencies when tests need control.
            • Builders are for step-by-step construction with validation; options objects are the lightweight default.
            • Cloning strategies differ in depth and prototype preservation; prove the behavior with real identity checks.

            Remember the one-liner.
            Choose the simplest creation shape that makes state, identity, validation, and copying rules obvious.

            Up next: Observer and pub/sub shows how parts of an app react to changes without tight coupling. Later lessons connect these ideas to structural and behavioral patterns, state management, and app architecture.

            CompleteFrontend Clear concepts. Working examples.