The JavaScript Factory Pattern: Complete Guide
A complete guide to the JavaScript factory pattern. Covers simple factories, factory methods, abstract factories, registration-based factories, async factories, and choosing the right factory variant for your application architecture.
The factory pattern creates objects without specifying the exact class or constructor to use. Instead of calling new directly, a factory function or method encapsulates the creation logic, deciding which type of object to instantiate based on parameters, configuration, or context.
For practical applications of dynamic object creation, see Creating Dynamic Objects with JS Factory Pattern.
Simple Factory
function createNotification(type, message, options = {}) {
const base = {
id: crypto.randomUUID(),
message,
createdAt: Date.now(),
read: false,
dismiss() {
this.read = true;
},
};
switch (type) {
case "success":
return {
...base,
type: "success",
icon: "check-circle",
duration: options.duration || 3000,
color: "#22c55e",
};
case "error":
return {
...base,
type: "error",
icon: "x-circle",
duration: options.duration || 0, // Errors persist
color: "#ef4444",
retry: options.onRetry || null,
};
case "warning":
return {
...base,
type: "warning",
icon: "alert-triangle",
duration: options.duration || 5000,
color: "#f59e0b",
};
case "info":
return {
...base,
type: "info",
icon: "info",
duration: options.duration || 4000,
color: "#3b82f6",
};
default:
throw new Error(`Unknown notification type: ${type}`);
}
}
// Usage
const success = createNotification("success", "File saved");
const error = createNotification("error", "Upload failed", {
onRetry: () => console.log("Retrying..."),
});Factory Method Pattern
class PaymentProcessor {
// Template method
async processPayment(amount, details) {
this.validate(amount, details);
const result = await this.executePayment(amount, details);
this.logTransaction(result);
return result;
}
validate(amount, details) {
if (amount <= 0) throw new Error("Amount must be positive");
if (!details) throw new Error("Payment details required");
}
logTransaction(result) {
console.log(`Transaction ${result.id}: ${result.status}`);
}
// Factory method -- subclasses override
executePayment(amount, details) {
throw new Error("Subclasses must implement executePayment");
}
}
class CreditCardProcessor extends PaymentProcessor {
async executePayment(amount, details) {
console.log(`Charging $${amount} to card ending ${details.last4}`);
return {
id: `cc_${Date.now()}`,
status: "completed",
method: "credit_card",
amount,
};
}
}
class PayPalProcessor extends PaymentProcessor {
async executePayment(amount, details) {
console.log(`PayPal charge: $${amount} to ${details.email}`);
return {
id: `pp_${Date.now()}`,
status: "completed",
method: "paypal",
amount,
};
}
}
class CryptoProcessor extends PaymentProcessor {
async executePayment(amount, details) {
console.log(`Crypto transfer: $${amount} to ${details.wallet}`);
return {
id: `crypto_${Date.now()}`,
status: "pending",
method: "crypto",
amount,
};
}
}
// Factory function to select processor
function createPaymentProcessor(method) {
const processors = {
credit_card: CreditCardProcessor,
paypal: PayPalProcessor,
crypto: CryptoProcessor,
};
const ProcessorClass = processors[method];
if (!ProcessorClass) throw new Error(`Unsupported method: ${method}`);
return new ProcessorClass();
}
// Usage
const processor = createPaymentProcessor("credit_card");
await processor.processPayment(99.99, { last4: "4242" });Abstract Factory
// Abstract factory for creating themed UI components
function createUIFactory(theme) {
const themes = {
light: {
createButton(label, onClick) {
return {
type: "button",
label,
onClick,
styles: {
background: "#ffffff",
color: "#1a1a1a",
border: "1px solid #e5e5e5",
borderRadius: "6px",
},
};
},
createInput(placeholder, onChange) {
return {
type: "input",
placeholder,
onChange,
styles: {
background: "#ffffff",
color: "#1a1a1a",
border: "1px solid #d4d4d4",
borderRadius: "6px",
},
};
},
createCard(title, content) {
return {
type: "card",
title,
content,
styles: {
background: "#ffffff",
color: "#1a1a1a",
boxShadow: "0 1px 3px rgba(0,0,0,0.12)",
borderRadius: "8px",
},
};
},
},
dark: {
createButton(label, onClick) {
return {
type: "button",
label,
onClick,
styles: {
background: "#2d2d2d",
color: "#f5f5f5",
border: "1px solid #404040",
borderRadius: "6px",
},
};
},
createInput(placeholder, onChange) {
return {
type: "input",
placeholder,
onChange,
styles: {
background: "#1a1a1a",
color: "#f5f5f5",
border: "1px solid #404040",
borderRadius: "6px",
},
};
},
createCard(title, content) {
return {
type: "card",
title,
content,
styles: {
background: "#2d2d2d",
color: "#f5f5f5",
boxShadow: "0 1px 3px rgba(0,0,0,0.4)",
borderRadius: "8px",
},
};
},
},
};
const factory = themes[theme];
if (!factory) throw new Error(`Unknown theme: ${theme}`);
return factory;
}
// Usage - all components share consistent theme
const ui = createUIFactory("dark");
const loginBtn = ui.createButton("Sign In", () => console.log("Login"));
const emailInput = ui.createInput("Email address", (e) => console.log(e));
const profileCard = ui.createCard("Profile", "User settings");Registration-Based Factory
class PluginFactory {
#registry = new Map();
#validators = new Map();
register(name, creator, validator = null) {
if (this.#registry.has(name)) {
throw new Error(`Plugin "${name}" already registered`);
}
this.#registry.set(name, creator);
if (validator) this.#validators.set(name, validator);
return this;
}
create(name, config = {}) {
const creator = this.#registry.get(name);
if (!creator) {
throw new Error(
`Unknown plugin: "${name}". Available: ${this.getRegistered().join(", ")}`
);
}
const validator = this.#validators.get(name);
if (validator && !validator(config)) {
throw new Error(`Invalid config for plugin "${name}"`);
}
return creator(config);
}
has(name) {
return this.#registry.has(name);
}
unregister(name) {
this.#registry.delete(name);
this.#validators.delete(name);
}
getRegistered() {
return [...this.#registry.keys()];
}
}
// Usage
const pluginFactory = new PluginFactory();
pluginFactory
.register(
"markdown",
(config) => ({
name: "markdown",
transform(content) {
return content.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>");
},
})
)
.register(
"syntax-highlight",
(config) => ({
name: "syntax-highlight",
language: config.language || "javascript",
transform(code) {
return `<pre><code class="lang-${config.language}">${code}</code></pre>`;
},
}),
(config) => typeof config.language === "string"
)
.register(
"image-optimizer",
(config) => ({
name: "image-optimizer",
quality: config.quality || 80,
async process(imagePath) {
console.log(`Optimizing ${imagePath} at quality ${config.quality}`);
return imagePath;
},
}),
(config) => config.quality >= 1 && config.quality <= 100
);
const mdPlugin = pluginFactory.create("markdown");
const highlighter = pluginFactory.create("syntax-highlight", { language: "python" });Async Factory
async function createDatabaseClient(config) {
const { type, connectionString, pool } = config;
const clients = {
async postgres() {
console.log(`Connecting to PostgreSQL: ${connectionString}`);
// Simulate async connection
await new Promise((r) => setTimeout(r, 100));
return {
type: "postgres",
async query(sql, params) {
return { rows: [], command: sql };
},
async transaction(fn) {
console.log("BEGIN");
try {
const result = await fn(this);
console.log("COMMIT");
return result;
} catch (err) {
console.log("ROLLBACK");
throw err;
}
},
async close() {
console.log("PostgreSQL connection closed");
},
};
},
async sqlite() {
console.log(`Opening SQLite: ${connectionString}`);
await new Promise((r) => setTimeout(r, 50));
return {
type: "sqlite",
async query(sql, params) {
return { rows: [], changes: 0 };
},
async close() {
console.log("SQLite connection closed");
},
};
},
async memory() {
const store = new Map();
return {
type: "memory",
async query(collection, filter) {
return [...(store.get(collection) || [])];
},
async insert(collection, doc) {
if (!store.has(collection)) store.set(collection, []);
store.get(collection).push(doc);
},
async close() {
store.clear();
},
};
},
};
const factory = clients[type];
if (!factory) throw new Error(`Unsupported database: ${type}`);
return factory();
}
// Usage
const db = await createDatabaseClient({
type: "postgres",
connectionString: "postgres://localhost/myapp",
pool: { min: 2, max: 10 },
});
const result = await db.query("SELECT * FROM users WHERE active = $1", [true]);
await db.close();| Factory Variant | Creation Logic | Extensibility | Complexity | Best For |
|---|---|---|---|---|
| Simple factory | Switch/map | Limited | Low | Known, fixed types |
| Factory method | Subclass override | Via inheritance | Medium | Framework extensibility |
| Abstract factory | Theme/family grouping | Via new factories | Medium | Consistent object families |
| Registration-based | Dynamic registry | Plugin-style | Medium | Open/extensible systems |
| Async factory | Async initialization | Per implementation | Medium | I/O-bound creation |
Rune AI
Key Insights
- Simple factories use switch or map to select object types: Centralized creation logic keeps type decisions in one place and hides implementation details from consumers
- Factory methods enable subclass-driven creation: Override a creation method in subclasses to change the type of objects without modifying the template algorithm
- Abstract factories produce consistent families of related objects: Theme factories, platform factories, and environment factories ensure all created objects share compatible styles
- Registration-based factories support open extensibility: Third-party code can register new types at runtime without modifying the factory source code
- Async factories handle I/O-bound initialization safely: Database connections, API clients, and file-based resources often require async setup before the object is ready to use
Frequently Asked Questions
When should I use a factory instead of calling new directly?
What is the difference between a factory function and a factory method?
How do factories work with TypeScript?
Can factories replace constructors entirely?
Conclusion
The factory pattern encapsulates object creation logic, making code flexible and extensible. Simple factories handle known types with switch statements. Factory methods use inheritance for framework extensibility. Abstract factories ensure consistent object families. Registration-based factories support plugin architectures. For dynamic object creation techniques, see Creating Dynamic Objects with JS Factory Pattern. For understanding how factories relate to the module pattern, see JavaScript Module Pattern: Advanced Tutorial.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.