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.
| Aspect | Constructor / class | Factory function |
|---|---|---|
| Invocation | Requires new | Plain call |
this binding | Implicit, easy to misuse | None of the usual this pitfalls |
| Return value | Returns the instance | Returns anything you like |
| Private state | Needs #fields | Natural via closures |
| Conditional types | Awkward | Trivial — return different shapes |
Forgetting
newon a constructor in non-strict code silently bindsthisto 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-relatedthisbugs. - 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/ifchains for selecting implementations, and validate unknown keys explicitly. - Reach for
classwhen you need many instances and want prototype-based method sharing for memory efficiency, or when subclassing withextendsis the clearer model. - Name factories with a
create*(ormake*) prefix so it’s obvious they return objects and aren’t called withnew.