How to implement factory pattern in JavaScript

The factory pattern creates objects without exposing instantiation logic, providing flexibility to choose which class to instantiate at runtime. As the creator of CoreUI with 26 years of JavaScript development experience, I’ve implemented factory patterns in large-scale applications to manage component creation, handle multiple database adapters, and provide plugin architectures for millions of users.

The most maintainable approach uses factory functions or classes that encapsulate object creation logic.

Basic Factory Function

// Product classes
class Car {
  constructor(make, model) {
    this.type = 'car'
    this.make = make
    this.model = model
  }

  drive() {
    console.log(`Driving ${this.make} ${this.model}`)
  }
}

class Truck {
  constructor(make, model) {
    this.type = 'truck'
    this.make = make
    this.model = model
  }

  drive() {
    console.log(`Driving ${this.make} ${this.model} truck`)
  }
}

// Factory function
function vehicleFactory(type, make, model) {
  switch (type) {
    case 'car':
      return new Car(make, model)
    case 'truck':
      return new Truck(make, model)
    default:
      throw new Error(`Unknown vehicle type: ${type}`)
  }
}

// Usage
const myCar = vehicleFactory('car', 'Toyota', 'Camry')
const myTruck = vehicleFactory('truck', 'Ford', 'F-150')

myCar.drive() // Driving Toyota Camry
myTruck.drive() // Driving Ford F-150 truck

Factory Class

class User {
  constructor(name, email, role) {
    this.name = name
    this.email = email
    this.role = role
  }

  getPermissions() {
    return []
  }
}

class Admin extends User {
  constructor(name, email) {
    super(name, email, 'admin')
  }

  getPermissions() {
    return ['read', 'write', 'delete', 'manage']
  }
}

class Editor extends User {
  constructor(name, email) {
    super(name, email, 'editor')
  }

  getPermissions() {
    return ['read', 'write']
  }
}

class Viewer extends User {
  constructor(name, email) {
    super(name, email, 'viewer')
  }

  getPermissions() {
    return ['read']
  }
}

// Factory class
class UserFactory {
  static create(role, name, email) {
    switch (role) {
      case 'admin':
        return new Admin(name, email)
      case 'editor':
        return new Editor(name, email)
      case 'viewer':
        return new Viewer(name, email)
      default:
        return new User(name, email, role)
    }
  }
}

// Usage
const admin = UserFactory.create('admin', 'John', '[email protected]')
const editor = UserFactory.create('editor', 'Jane', '[email protected]')

console.log(admin.getPermissions())
// ['read', 'write', 'delete', 'manage']

console.log(editor.getPermissions())
// ['read', 'write']

Abstract Factory

// Database adapters
class MySQLAdapter {
  connect() {
    return 'Connected to MySQL'
  }

  query(sql) {
    return `MySQL query: ${sql}`
  }
}

class PostgreSQLAdapter {
  connect() {
    return 'Connected to PostgreSQL'
  }

  query(sql) {
    return `PostgreSQL query: ${sql}`
  }
}

class MongoDBAdapter {
  connect() {
    return 'Connected to MongoDB'
  }

  query(json) {
    return `MongoDB query: ${JSON.stringify(json)}`
  }
}

// Abstract factory
class DatabaseFactory {
  static create(type, config) {
    const adapters = {
      mysql: MySQLAdapter,
      postgresql: PostgreSQLAdapter,
      mongodb: MongoDBAdapter
    }

    const Adapter = adapters[type]

    if (!Adapter) {
      throw new Error(`Unknown database type: ${type}`)
    }

    return new Adapter(config)
  }
}

// Usage
const mysql = DatabaseFactory.create('mysql', { host: 'localhost' })
console.log(mysql.connect()) // Connected to MySQL
console.log(mysql.query('SELECT * FROM users'))
// MySQL query: SELECT * FROM users

const mongo = DatabaseFactory.create('mongodb', { host: 'localhost' })
console.log(mongo.connect()) // Connected to MongoDB
console.log(mongo.query({ find: 'users' }))
// MongoDB query: {"find":"users"}

Factory with Registration

class ComponentFactory {
  constructor() {
    this.components = new Map()
  }

  register(name, component) {
    this.components.set(name, component)
  }

  create(name, props) {
    const Component = this.components.get(name)

    if (!Component) {
      throw new Error(`Component not registered: ${name}`)
    }

    return new Component(props)
  }
}

// Component classes
class Button {
  constructor(props) {
    this.type = 'button'
    this.label = props.label
  }

  render() {
    return `<button>${this.label}</button>`
  }
}

class Input {
  constructor(props) {
    this.type = 'input'
    this.placeholder = props.placeholder
  }

  render() {
    return `<input placeholder="${this.placeholder}">`
  }
}

// Usage
const factory = new ComponentFactory()

factory.register('button', Button)
factory.register('input', Input)

const btn = factory.create('button', { label: 'Click Me' })
const input = factory.create('input', { placeholder: 'Enter text' })

console.log(btn.render()) // <button>Click Me</button>
console.log(input.render()) // <input placeholder="Enter text">

Factory with Configuration

class Logger {
  constructor(config) {
    this.level = config.level
    this.output = config.output
  }

  log(message) {
    if (this.output === 'console') {
      console.log(`[${this.level}] ${message}`)
    } else if (this.output === 'file') {
      // Write to file
      console.log(`Writing to file: [${this.level}] ${message}`)
    }
  }
}

class LoggerFactory {
  static create(environment) {
    const configs = {
      development: {
        level: 'debug',
        output: 'console'
      },
      production: {
        level: 'error',
        output: 'file'
      },
      test: {
        level: 'silent',
        output: 'console'
      }
    }

    const config = configs[environment] || configs.development

    return new Logger(config)
  }
}

// Usage
const devLogger = LoggerFactory.create('development')
devLogger.log('Debug message') // [debug] Debug message

const prodLogger = LoggerFactory.create('production')
prodLogger.log('Error occurred') // Writing to file: [error] Error occurred

Async Factory

class ApiClient {
  constructor(config) {
    this.baseUrl = config.baseUrl
    this.token = config.token
  }

  async get(endpoint) {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      headers: { Authorization: `Bearer ${this.token}` }
    })
    return response.json()
  }
}

class ApiClientFactory {
  static async create(service) {
    // Fetch configuration from server
    const config = await fetch(`/api/config/${service}`).then(res => res.json())

    return new ApiClient(config)
  }
}

// Usage
const client = await ApiClientFactory.create('users')
const users = await client.get('/users')
console.log(users)

Factory with Dependency Injection

class EmailService {
  send(to, subject, body) {
    console.log(`Sending email to ${to}`)
  }
}

class SMSService {
  send(phone, message) {
    console.log(`Sending SMS to ${phone}`)
  }
}

class NotificationFactory {
  constructor(emailService, smsService) {
    this.services = {
      email: emailService,
      sms: smsService
    }
  }

  create(type) {
    const service = this.services[type]

    if (!service) {
      throw new Error(`Unknown notification type: ${type}`)
    }

    return service
  }
}

// Usage
const emailService = new EmailService()
const smsService = new SMSService()
const factory = new NotificationFactory(emailService, smsService)

const email = factory.create('email')
email.send('[email protected]', 'Hello', 'Welcome!')
// Sending email to [email protected]

const sms = factory.create('sms')
sms.send('+1234567890', 'Your code is 1234')
// Sending SMS to +1234567890

Plugin Factory

class PluginFactory {
  constructor() {
    this.plugins = new Map()
  }

  register(name, plugin) {
    this.plugins.set(name, plugin)
  }

  create(name, options) {
    const Plugin = this.plugins.get(name)

    if (!Plugin) {
      return null // Gracefully handle missing plugins
    }

    return new Plugin(options)
  }

  has(name) {
    return this.plugins.has(name)
  }

  getAll() {
    return Array.from(this.plugins.keys())
  }
}

// Plugin classes
class AnalyticsPlugin {
  constructor(options) {
    this.trackingId = options.trackingId
  }

  track(event) {
    console.log(`Tracking: ${event} (ID: ${this.trackingId})`)
  }
}

class CachePlugin {
  constructor(options) {
    this.ttl = options.ttl
    this.cache = new Map()
  }

  get(key) {
    return this.cache.get(key)
  }

  set(key, value) {
    this.cache.set(key, value)
  }
}

// Usage
const factory = new PluginFactory()

factory.register('analytics', AnalyticsPlugin)
factory.register('cache', CachePlugin)

const analytics = factory.create('analytics', { trackingId: 'UA-123456' })
analytics.track('page_view') // Tracking: page_view (ID: UA-123456)

const cache = factory.create('cache', { ttl: 3600 })
cache.set('user:1', { name: 'John' })
console.log(cache.get('user:1')) // { name: 'John' }

console.log(factory.getAll()) // ['analytics', 'cache']

Best Practice Note

This is how we implement factory patterns in CoreUI for flexible component creation. The factory pattern provides a clean abstraction for object creation, allowing runtime decisions about which class to instantiate without coupling code to specific implementations. Always use factories when you need runtime class selection, multiple implementations of an interface, or centralized object creation logic. Consider registration-based factories for plugin systems and dependency injection for testability.

For related patterns, check out how to implement singleton pattern in JavaScript and how to implement builder pattern in JavaScript.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author