Skip to content
JavaScript js patterns 4 min read

Factory Pattern

The factory pattern centralizes object creation behind a function, so callers ask for what they want rather than wiring up the construction details themselves. In JavaScript this is especially natural because a plain function can return any object — no new keyword, no class hierarchy required. This decouples consuming code from concrete implementations, making it easy to swap, configure, or extend the things you build.

Factory functions vs constructors and classes

A constructor (or class) is invoked with new, implicitly creates this, links the prototype, and returns the new instance. A factory function is just a regular function that explicitly returns an object. The caller never has to know whether new was needed.

// Constructor / class
class UserClass {
  constructor(name) {
    this.name = name;
  }
  greet() {
    return `Hi, I'm ${this.name}`;
  }
}
const a = new UserClass("Ada");

// Factory function — no `new`, returns the object directly
function createUser(name) {
  return {
    name,
    greet() {
      return `Hi, I'm ${this.name}`;
    },
  };
}
const b = createUser("Grace");

console.log(a.greet());
console.log(b.greet());

Output:

Hi, I'm Ada
Hi, I'm Grace

Both work, but the factory has practical advantages.

AspectConstructor / classFactory function
InvocationRequires newPlain call
this bindingImplicit, easy to misuseNone of the usual this pitfalls
Return valueReturns the instanceReturns anything you like
Private stateNeeds #fieldsNatural via closures
Conditional typesAwkwardTrivial — return different shapes

Forgetting new on a constructor in non-strict code silently binds this to the global object and corrupts state. Factory functions sidestep this class of bug entirely.

Returning configured objects

A factory shines when construction involves defaults, validation, or derived data. The caller passes intent; the factory assembles a fully formed object.

function createButton({ label, variant = "primary", disabled = false } = {}) {
  const classes = ["btn", `btn-${variant}`];
  if (disabled) classes.push("btn-disabled");

  return {
    label,
    variant,
    disabled,
    className: classes.join(" "),
    render() {
      return `<button class="${this.className}"${
        this.disabled ? " disabled" : ""
      }>${this.label}</button>`;
    },
  };
}

const save = createButton({ label: "Save" });
const cancel = createButton({ label: "Cancel", variant: "ghost", disabled: true });

console.log(save.render());
console.log(cancel.render());

Output:

<button class="btn btn-primary">Save</button>
<button class="btn btn-ghost btn-disabled" disabled>Cancel</button>

Because closures capture local variables, factories also give you genuine private state without exposing it on the returned object:

function createCounter(start = 0) {
  let count = start; // private — not reachable from outside

  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    get value() {
      return count;
    },
  };
}

const counter = createCounter(10);
counter.increment();
counter.increment();
console.log(counter.value);
console.log(counter.count); // no such property

Output:

12
undefined

Polymorphic creation

The most powerful use of the pattern is choosing which object to build at runtime. A single entry point returns different implementations that share a common interface, so the rest of your code stays agnostic.

const notifiers = {
  email: (to) => ({
    send: (msg) => `Email to ${to}: ${msg}`,
  }),
  sms: (to) => ({
    send: (msg) => `SMS to ${to}: ${msg}`,
  }),
  push: (to) => ({
    send: (msg) => `Push to device ${to}: ${msg}`,
  }),
};

function createNotifier(channel, to) {
  const build = notifiers[channel];
  if (!build) {
    throw new Error(`Unknown channel: ${channel}`);
  }
  return build(to);
}

const channels = [
  createNotifier("email", "[email protected]"),
  createNotifier("sms", "+15550100"),
  createNotifier("push", "dev-42"),
];

for (const n of channels) {
  console.log(n.send("Build passed"));
}

Output:

Email to [email protected]: Build passed
SMS to +15550100: Build passed
Push to device dev-42: Build passed

Adding a new channel means adding one entry to the notifiers map — no switch statements scattered across the codebase, and no caller changes. This registry-style factory keeps the open/closed principle within reach.

Best Practices

  • Prefer factory functions when you want private state, conditional return types, or to avoid new-related this bugs.
  • Accept a single options object with destructured defaults rather than long positional argument lists.
  • Keep the returned objects to a consistent shared interface so polymorphic callers stay simple.
  • Use a lookup map (registry) instead of switch/if chains for selecting implementations, and validate unknown keys explicitly.
  • Reach for class when you need many instances and want prototype-based method sharing for memory efficiency, or when subclassing with extends is the clearer model.
  • Name factories with a create* (or make*) prefix so it’s obvious they return objects and aren’t called with new.
Last updated June 1, 2026
Was this helpful?