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.

JavaScriptadvanced
16 min read

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

javascriptjavascript
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

javascriptjavascript
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

javascriptjavascript
// 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

javascriptjavascript
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

javascriptjavascript
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 VariantCreation LogicExtensibilityComplexityBest For
Simple factorySwitch/mapLimitedLowKnown, fixed types
Factory methodSubclass overrideVia inheritanceMediumFramework extensibility
Abstract factoryTheme/family groupingVia new factoriesMediumConsistent object families
Registration-basedDynamic registryPlugin-styleMediumOpen/extensible systems
Async factoryAsync initializationPer implementationMediumI/O-bound creation
Rune AI

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
RunePowered by Rune AI

Frequently Asked Questions

When should I use a factory instead of calling new directly?

Use a factory when creation involves complex logic (deciding which class to instantiate), when you need to decouple the consumer from the concrete implementation, when objects require async initialization, or when different environments need different implementations (production vs test). For simple objects with no variation, `new` is perfectly fine.

What is the difference between a factory function and a factory method?

factory function is a standalone function that creates and returns objects. A factory method is a method on a class that subclasses override to change the type of objects created. Factory functions are more common in JavaScript because they leverage closures and do not require class hierarchies. Factory methods are useful when you want a template method pattern with customizable creation steps.

How do factories work with TypeScript?

TypeScript enhances factories with return type inference and discriminated unions. You can type the factory function's return based on the input parameter using conditional types or overloads. For registration-based factories, use generics to maintain type safety: `register<T>(name: string, creator: () => T)` ensures `create(name)` returns the correct type.

Can factories replace constructors entirely?

Many JavaScript style guides (including Airbnb's) prefer factory functions over classes because they avoid the `this` keyword, support true private state via closures, and cannot be called incorrectly (no `new` keyword needed). However, classes provide clear syntax for inheritance, static methods, and private fields (#). Use whichever style fits your codebase conventions.

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.