The Factory Pattern
The factory pattern hands the responsibility of creating objects to a dedicated function or method instead of scattering new calls throughout your code. Callers ask for “a logger” or “a database client” and receive a ready-to-use instance without knowing—or caring—which concrete class backs it. This decoupling keeps construction logic in one place, makes swapping implementations trivial, and is one of the most natural patterns to express in JavaScript, where functions are first-class and new is optional.
Why factories matter
In a small program, calling a constructor directly is fine. The trouble starts when the choice of which object to build depends on configuration, environment, or runtime input. Sprinkling if (env === 'prod') new X() else new Y() across modules duplicates logic and couples every caller to every concrete type. A factory centralizes that decision so the rest of the application depends only on a stable interface.
Factories also let you do work that a bare constructor cannot: return cached instances, return a subclass, validate input before building, or hide the new keyword entirely. Because a JavaScript function can return any object, a factory is free to pick the right implementation at call time.
A simple factory function
The most common form is a plain function that takes input and returns an object. Here we create different logger objects based on a level argument.
// logger.js
function createLogger(level = "info") {
const levels = { error: 0, warn: 1, info: 2, debug: 3 };
const threshold = levels[level] ?? levels.info;
const log = (msgLevel, ...args) => {
if (levels[msgLevel] <= threshold) {
console.log(`[${msgLevel.toUpperCase()}]`, ...args);
}
};
return {
error: (...a) => log("error", ...a),
warn: (...a) => log("warn", ...a),
info: (...a) => log("info", ...a),
debug: (...a) => log("debug", ...a),
};
}
export { createLogger };
// app.js
import { createLogger } from "./logger.js";
const logger = createLogger("warn");
logger.error("Disk almost full");
logger.warn("Memory at 85%");
logger.info("Request handled"); // suppressed: below threshold
logger.debug("Cache miss"); // suppressed
Output:
[ERROR] Disk almost full
[WARN] Memory at 85%
The caller never touches the internal levels map or the filtering logic—it just asks for a logger and uses it.
The factory method pattern
The factory method pattern uses a method (often on a class or base object) that subclasses or callers override to decide which object to produce. It is useful when the surrounding workflow is fixed but the concrete product varies.
class NotificationCenter {
// Factory method — subclasses decide the concrete channel
createChannel() {
throw new Error("createChannel() must be implemented");
}
async notify(message) {
const channel = this.createChannel();
await channel.send(message);
}
}
class EmailCenter extends NotificationCenter {
createChannel() {
return { send: async (m) => console.log(`Email sent: ${m}`) };
}
}
class SmsCenter extends NotificationCenter {
createChannel() {
return { send: async (m) => console.log(`SMS sent: ${m}`) };
}
}
await new EmailCenter().notify("Order shipped");
await new SmsCenter().notify("Code: 4821");
Output:
Email sent: Order shipped
SMS sent: Code: 4821
The notify workflow stays identical; only createChannel changes per subclass.
A real-world example: database clients
A common use is selecting a database driver from configuration. The factory reads an environment variable and returns a uniform client interface, so the rest of the app is driver-agnostic.
// db-factory.js
class PostgresClient {
constructor(url) { this.url = url; }
async query(sql) { return `pg<${this.url}>: ${sql}`; }
}
class SqliteClient {
constructor(file) { this.file = file; }
async query(sql) { return `sqlite<${this.file}>: ${sql}`; }
}
const builders = {
postgres: () => new PostgresClient(process.env.PG_URL ?? "localhost:5432"),
sqlite: () => new SqliteClient(process.env.SQLITE_FILE ?? "app.db"),
};
export function createDbClient(driver = process.env.DB_DRIVER ?? "sqlite") {
const build = builders[driver];
if (!build) throw new Error(`Unknown DB driver: ${driver}`);
return build();
}
// usage.js
import { createDbClient } from "./db-factory.js";
const db = createDbClient("postgres");
console.log(await db.query("SELECT 1"));
Output:
pg<localhost:5432>: SELECT 1
Tip: Map the lookup table (
builders) to factory functions, not to pre-built instances. Eager instantiation would open every database connection at module load, even drivers you never use.
Abstract factory
When you need to create families of related objects that must be used together, reach for an abstract factory: a factory that returns a set of coordinated factories. For example, a createStorage() factory might return matched fileStore and metadataStore objects that share the same backend, guaranteeing they never get mixed.
function createStorage(backend) {
if (backend === "s3") {
return {
files: { put: (k) => `s3:put ${k}` },
meta: { tag: (k) => `s3:tag ${k}` },
};
}
return {
files: { put: (k) => `disk:put ${k}` },
meta: { tag: (k) => `disk:tag ${k}` },
};
}
const store = createStorage("s3");
console.log(store.files.put("photo.jpg"));
console.log(store.meta.tag("photo.jpg"));
Output:
s3:put photo.jpg
s3:tag photo.jpg
Factory variants compared
| Variant | What varies | Typical trigger |
|---|---|---|
| Simple factory | A single product type | A function argument or config value |
| Factory method | One product, chosen by subclass | Inheritance / overriding |
| Abstract factory | A family of related products | Whole backend or theme swap |
Note: In CommonJS, the same patterns apply—just replace
export/importwithmodule.exports = { createDbClient }andconst { createDbClient } = require("./db-factory"). Factory functions are a great fit forrequire, since the module exports a builder rather than a singleton instance.
Best practices
- Return a small, stable interface from your factory so callers never depend on concrete classes.
- Prefer plain factory functions over classes in JavaScript unless you genuinely need
instanceofor prototype sharing. - Validate and normalize input inside the factory, and throw early on unknown types rather than returning
undefined. - Use a lookup map of builder functions instead of long
if/switchchains for cleaner, lazily-evaluated selection. - Keep factories side-effect-free where possible; defer expensive work (connections, file handles) until the returned object is actually used.
- Combine factories with dependency injection so tests can swap in fake products without monkey-patching.