Skip to main content

Creational Design Patterns in JavaScript

Created: May 8, 2026 Larry Qu 7 min read

Creational patterns focus on object creation mechanisms. This article covers Singleton, Factory, Abstract Factory, Builder, and Prototype patterns.

Introduction

Creational patterns provide:

  • Flexible object creation
  • Encapsulation of creation logic
  • Reduced coupling
  • Reusable creation mechanisms
  • Controlled instantiation

Understanding these patterns helps you:

  • Create objects efficiently
  • Manage object creation
  • Implement flexible systems
  • Reduce code duplication
  • Improve maintainability

Singleton Pattern

Basic Singleton

// ✅ Good: Basic Singleton
class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }

    this.connection = null;
    Database.instance = this;
  }

  connect() {
    this.connection = 'Connected to database';
    console.log(this.connection);
  }
}

const db1 = new Database();
const db2 = new Database();

console.log(db1 === db2); // true (same instance)
db1.connect(); // Connected to database

Lazy Singleton

// ✅ Good: Lazy initialization
class Logger {
  static getInstance() {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }

  log(message) {
    console.log(`[LOG] ${message}`);
  }
}

// Instance created only when needed
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();

console.log(logger1 === logger2); // true
logger1.log('Hello'); // [LOG] Hello

Module Pattern (Singleton)

// ✅ Good: Module pattern for Singleton
const ConfigManager = (() => {
  let instance;

  function createInstance() {
    return {
      config: {},
      get(key) {
        return this.config[key];
      },
      set(key, value) {
        this.config[key] = value;
      }
    };
  }

  return {
    getInstance() {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    }
  };
})();

const config1 = ConfigManager.getInstance();
const config2 = ConfigManager.getInstance();

console.log(config1 === config2); // true
config1.set('apiUrl', 'https://api.example.com');
console.log(config2.get('apiUrl')); // https://api.example.com

Factory Pattern

Simple Factory

// ✅ Good: Simple Factory
class AnimalFactory {
  static createAnimal(type) {
    switch (type) {
      case 'dog':
        return new Dog();
      case 'cat':
        return new Cat();
      case 'bird':
        return new Bird();
      default:
        throw new Error(`Unknown animal type: ${type}`);
    }
  }
}

class Dog {
  speak() {
    return 'Woof!';
  }
}

class Cat {
  speak() {
    return 'Meow!';
  }
}

class Bird {
  speak() {
    return 'Tweet!';
  }
}

// Usage
const dog = AnimalFactory.createAnimal('dog');
console.log(dog.speak()); // Woof!

const cat = AnimalFactory.createAnimal('cat');
console.log(cat.speak()); // Meow!

Factory Method

// ✅ Good: Factory Method pattern
class Creator {
  createProduct() {
    throw new Error('Must implement createProduct');
  }

  operation() {
    const product = this.createProduct();
    return product.operation();
  }
}

class ConcreteCreatorA extends Creator {
  createProduct() {
    return new ConcreteProductA();
  }
}

class ConcreteCreatorB extends Creator {
  createProduct() {
    return new ConcreteProductB();
  }
}

class ConcreteProductA {
  operation() {
    return 'Result from Product A';
  }
}

class ConcreteProductB {
  operation() {
    return 'Result from Product B';
  }
}

// Usage
const creatorA = new ConcreteCreatorA();
console.log(creatorA.operation()); // Result from Product A

const creatorB = new ConcreteCreatorB();
console.log(creatorB.operation()); // Result from Product B

Dynamic Factory

// ✅ Good: Dynamic factory with registration
class DynamicFactory {
  constructor() {
    this.creators = {};
  }

  register(type, creator) {
    this.creators[type] = creator;
  }

  create(type, ...args) {
    const creator = this.creators[type];
    if (!creator) {
      throw new Error(`Unknown type: ${type}`);
    }
    return new creator(...args);
  }
}

class User {
  constructor(name) {
    this.name = name;
  }
}

class Admin {
  constructor(name) {
    this.name = name;
    this.role = 'admin';
  }
}

// Usage
const factory = new DynamicFactory();
factory.register('user', User);
factory.register('admin', Admin);

const user = factory.create('user', 'John');
const admin = factory.create('admin', 'Jane');

console.log(user); // User { name: 'John' }
console.log(admin); // Admin { name: 'Jane', role: 'admin' }

Abstract Factory Pattern

// ✅ Good: Abstract Factory
class UIFactory {
  createButton() {
    throw new Error('Must implement createButton');
  }

  createCheckbox() {
    throw new Error('Must implement createCheckbox');
  }
}

class WindowsUIFactory extends UIFactory {
  createButton() {
    return new WindowsButton();
  }

  createCheckbox() {
    return new WindowsCheckbox();
  }
}

class MacUIFactory extends UIFactory {
  createButton() {
    return new MacButton();
  }

  createCheckbox() {
    return new MacCheckbox();
  }
}

class WindowsButton {
  render() {
    return 'Windows Button';
  }
}

class MacButton {
  render() {
    return 'Mac Button';
  }
}

class WindowsCheckbox {
  render() {
    return 'Windows Checkbox';
  }
}

class MacCheckbox {
  render() {
    return 'Mac Checkbox';
  }
}

// Usage
function createUI(factory) {
  const button = factory.createButton();
  const checkbox = factory.createCheckbox();
  return { button, checkbox };
}

const windowsUI = createUI(new WindowsUIFactory());
console.log(windowsUI.button.render()); // Windows Button
console.log(windowsUI.checkbox.render()); // Windows Checkbox

const macUI = createUI(new MacUIFactory());
console.log(macUI.button.render()); // Mac Button
console.log(macUI.checkbox.render()); // Mac Checkbox

Builder Pattern

Basic Builder

// ✅ Good: Builder pattern
class User {
  constructor(builder) {
    this.name = builder.name;
    this.email = builder.email;
    this.age = builder.age;
    this.role = builder.role;
  }
}

class UserBuilder {
  constructor(name) {
    this.name = name;
  }

  setEmail(email) {
    this.email = email;
    return this;
  }

  setAge(age) {
    this.age = age;
    return this;
  }

  setRole(role) {
    this.role = role;
    return this;
  }

  build() {
    return new User(this);
  }
}

// Usage
const user = new UserBuilder('John')
  .setEmail('[email protected]')
  .setAge(30)
  .setRole('admin')
  .build();

console.log(user);
// User {
//   name: 'John',
//   email: '[email protected]',
//   age: 30,
//   role: 'admin'
// }

Complex Builder

// ✅ Good: Complex builder for configuration
class DatabaseConfig {
  constructor(builder) {
    this.host = builder.host;
    this.port = builder.port;
    this.database = builder.database;
    this.username = builder.username;
    this.password = builder.password;
    this.ssl = builder.ssl;
    this.timeout = builder.timeout;
    this.pool = builder.pool;
  }
}

class DatabaseConfigBuilder {
  constructor(host) {
    this.host = host;
    this.port = 5432;
    this.ssl = false;
    this.timeout = 5000;
    this.pool = 10;
  }

  setPort(port) {
    this.port = port;
    return this;
  }

  setDatabase(database) {
    this.database = database;
    return this;
  }

  setCredentials(username, password) {
    this.username = username;
    this.password = password;
    return this;
  }

  enableSSL() {
    this.ssl = true;
    return this;
  }

  setTimeout(timeout) {
    this.timeout = timeout;
    return this;
  }

  setPoolSize(pool) {
    this.pool = pool;
    return this;
  }

  build() {
    return new DatabaseConfig(this);
  }
}

// Usage
const config = new DatabaseConfigBuilder('localhost')
  .setPort(5432)
  .setDatabase('myapp')
  .setCredentials('user', 'password')
  .enableSSL()
  .setTimeout(10000)
  .setPoolSize(20)
  .build();

console.log(config);

Prototype Pattern

Basic Prototype

// ✅ Good: Prototype pattern
class User {
  constructor(name, email) {
    this.name = name;
    this.email = email;
  }

  clone() {
    return new User(this.name, this.email);
  }

  getInfo() {
    return `${this.name} (${this.email})`;
  }
}

// Usage
const user1 = new User('John', '[email protected]');
const user2 = user1.clone();

user2.name = 'Jane';

console.log(user1.getInfo()); // John ([email protected])
console.log(user2.getInfo()); // Jane ([email protected])

Deep Clone Prototype

// ✅ Good: Deep clone prototype
class ComplexObject {
  constructor(data) {
    this.data = data;
  }

  clone() {
    return new ComplexObject(JSON.parse(JSON.stringify(this.data)));
  }
}

// Usage
const original = new ComplexObject({
  name: 'John',
  hobbies: ['reading', 'coding'],
  address: { city: 'New York', zip: '10001' }
});

const cloned = original.clone();
cloned.data.hobbies.push('gaming');
cloned.data.address.city = 'Boston';

console.log(original.data);
// { name: 'John', hobbies: ['reading', 'coding'], address: { city: 'New York', zip: '10001' } }

console.log(cloned.data);
// { name: 'John', hobbies: ['reading', 'coding', 'gaming'], address: { city: 'Boston', zip: '10001' } }

Practical Examples

Logger Factory

// ✅ Good: Logger factory
class Logger {
  log(message) {
    throw new Error('Must implement log');
  }
}

class ConsoleLogger extends Logger {
  log(message) {
    console.log(`[CONSOLE] ${message}`);
  }
}

class FileLogger extends Logger {
  log(message) {
    console.log(`[FILE] ${message}`);
  }
}

class LoggerFactory {
  static createLogger(type) {
    switch (type) {
      case 'console':
        return new ConsoleLogger();
      case 'file':
        return new FileLogger();
      default:
        throw new Error(`Unknown logger type: ${type}`);
    }
  }
}

// Usage
const consoleLogger = LoggerFactory.createLogger('console');
consoleLogger.log('Hello'); // [CONSOLE] Hello

const fileLogger = LoggerFactory.createLogger('file');
fileLogger.log('Hello'); // [FILE] Hello

HTTP Client Builder

// ✅ Good: HTTP client builder
class HttpClient {
  constructor(builder) {
    this.baseUrl = builder.baseUrl;
    this.timeout = builder.timeout;
    this.headers = builder.headers;
    this.interceptors = builder.interceptors;
  }

  async get(url) {
    console.log(`GET ${this.baseUrl}${url}`);
  }
}

class HttpClientBuilder {
  constructor(baseUrl) {
    this.baseUrl = baseUrl;
    this.timeout = 5000;
    this.headers = {};
    this.interceptors = [];
  }

  setTimeout(timeout) {
    this.timeout = timeout;
    return this;
  }

  addHeader(key, value) {
    this.headers[key] = value;
    return this;
  }

  addInterceptor(interceptor) {
    this.interceptors.push(interceptor);
    return this;
  }

  build() {
    return new HttpClient(this);
  }
}

// Usage
const client = new HttpClientBuilder('https://api.example.com')
  .setTimeout(10000)
  .addHeader('Authorization', 'Bearer token')
  .addHeader('Content-Type', 'application/json')
  .addInterceptor((request) => console.log('Request:', request))
  .build();

client.get('/users');

Best Practices

  1. Use Factory for object creation:
    // ✅ Good
    const obj = Factory.create(type);
    
    // ❌ Bad
    const obj = new ConcreteClass();
    ```javascript
    
  2. Use Builder for complex objects:
    // ✅ Good
    const obj = new Builder()
      .setProperty1(value1)
      .setProperty2(value2)
      .build();
    
    // ❌ Bad
    const obj = new ComplexClass(value1, value2, value3, ...);
    ```javascript
    
  3. Use Singleton for shared resources:
    // ✅ Good
    const db = Database.getInstance();
    
    // ❌ Bad
    const db = new Database();
    ```javascript
    

Common Mistakes

  1. Overusing Singleton:
    // ❌ Bad - everything as Singleton
    class Service { }
    Service.instance = new Service();
    
    // ✅ Good - use when necessary
    class Database { }
    Database.instance = new Database();
    ```javascript
    
  2. Builder without fluent interface:
    // ❌ Bad
    const builder = new Builder();
    builder.setName('John');
    builder.setEmail('[email protected]');
    
    // ✅ Good
    const obj = new Builder()
      .setName('John')
      .setEmail('[email protected]')
      .build();
    

Summary

Creational patterns manage object creation. Key takeaways:

  • Singleton: Single instance
  • Factory: Encapsulate creation
  • Abstract Factory: Family of objects
  • Builder: Complex object construction
  • Prototype: Clone objects
  • Improves flexibility
  • Reduces coupling
  • Encapsulates creation logic

Next Steps

Resources

Comments

Share this article

Scan to read on mobile