Learning Guides
Menu

Creational Design Patterns

11 min readNode.js Design Patterns

Creational Design Patterns

Creational patterns deal with object creation mechanisms. In Node.js, they help manage module dependencies, configuration, and complex object construction.

Factory Pattern

The Factory pattern encapsulates object creation logic. Instead of using new directly, you call a factory function that returns instances.

Simple Factory

JAVASCRIPT
function createUser(type, data) {
  switch (type) {
    case "admin":
      return new AdminUser(data);
    case "guest":
      return new GuestUser(data);
    default:
      return new StandardUser(data);
  }
}
 
// Usage
const admin = createUser("admin", { name: "Alice" });
const guest = createUser("guest", { name: "Bob" });

Why Use Factories?

  1. Decouple creation from usage: Callers don't need to know concrete classes
  2. Centralize object construction: One place to add logging, validation, caching
  3. Enable dynamic instantiation: Create objects based on runtime conditions
  4. Hide complexity: Complex construction logic stays in the factory

Database Connection Factory

JAVASCRIPT
// factory.js
const databases = {
  postgres: require("./adapters/postgres"),
  mysql: require("./adapters/mysql"),
  sqlite: require("./adapters/sqlite"),
};
 
function createDatabase(config) {
  const { type, ...options } = config;
 
  const Adapter = databases[type];
  if (!Adapter) {
    throw new Error(`Unknown database type: ${type}`);
  }
 
  // All adapters have the same interface
  return new Adapter(options);
}
 
module.exports = createDatabase;
 
// usage.js
const createDatabase = require("./factory");
 
const db = createDatabase({
  type: "postgres",
  host: "localhost",
  database: "myapp",
});
 
// Same interface regardless of database type
await db.query("SELECT * FROM users");
await db.close();

Factory with Closures

Factories can encapsulate private state:

JAVASCRIPT
function createCounter(initialValue = 0) {
  let count = initialValue; // Private
 
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getValue() {
      return count;
    },
    reset() {
      count = initialValue;
    },
  };
}
 
const counter = createCounter(10);
counter.increment(); // 11
counter.getValue(); // 11
// No way to access `count` directly

Builder Pattern

The Builder pattern separates complex object construction from its representation. Useful when objects have many optional parameters.

The Problem

JAVASCRIPT
// Constructor with many optional params - hard to read
const server = new HttpServer(
  3000,           // port
  'localhost',    // host
  true,           // https
  '/certs/key',   // keyPath
  '/certs/cert',  // certPath
  30000,          // timeout
  100,            // maxConnections
  true,           // keepAlive
  null,           // logger
  ...             // more options
);

Builder Solution

JAVASCRIPT
class HttpServerBuilder {
  constructor() {
    this.port = 80;
    this.host = "localhost";
    this.https = false;
    this.timeout = 30000;
    this.maxConnections = 100;
  }
 
  setPort(port) {
    this.port = port;
    return this; // Enable chaining
  }
 
  setHost(host) {
    this.host = host;
    return this;
  }
 
  enableHttps(keyPath, certPath) {
    this.https = true;
    this.keyPath = keyPath;
    this.certPath = certPath;
    return this;
  }
 
  setTimeout(timeout) {
    this.timeout = timeout;
    return this;
  }
 
  setMaxConnections(max) {
    this.maxConnections = max;
    return this;
  }
 
  build() {
    return new HttpServer(this);
  }
}
 
// Usage - fluent, readable
const server = new HttpServerBuilder()
  .setPort(443)
  .setHost("0.0.0.0")
  .enableHttps("/certs/key.pem", "/certs/cert.pem")
  .setTimeout(60000)
  .build();

Functional Builder

A more JavaScript-idiomatic approach:

JAVASCRIPT
function createHttpServer(configure) {
  const config = {
    port: 80,
    host: "localhost",
    https: false,
    timeout: 30000,
    maxConnections: 100,
  };
 
  // Builder methods
  const builder = {
    port: (p) => {
      config.port = p;
      return builder;
    },
    host: (h) => {
      config.host = h;
      return builder;
    },
    https: (key, cert) => {
      config.https = true;
      config.keyPath = key;
      config.certPath = cert;
      return builder;
    },
    timeout: (t) => {
      config.timeout = t;
      return builder;
    },
    maxConnections: (m) => {
      config.maxConnections = m;
      return builder;
    },
  };
 
  configure(builder);
 
  return new HttpServer(config);
}
 
// Usage
const server = createHttpServer((b) =>
  b
    .port(443)
    .host("0.0.0.0")
    .https("/certs/key.pem", "/certs/cert.pem")
    .timeout(60000),
);

Query Builder

JAVASCRIPT
class QueryBuilder {
  constructor(table) {
    this.table = table;
    this.columns = ["*"];
    this.conditions = [];
    this.orderByClause = null;
    this.limitValue = null;
  }
 
  select(...columns) {
    this.columns = columns;
    return this;
  }
 
  where(column, operator, value) {
    this.conditions.push({ column, operator, value });
    return this;
  }
 
  orderBy(column, direction = "ASC") {
    this.orderByClause = { column, direction };
    return this;
  }
 
  limit(value) {
    this.limitValue = value;
    return this;
  }
 
  build() {
    let query = `SELECT ${this.columns.join(", ")} FROM ${this.table}`;
 
    if (this.conditions.length) {
      const where = this.conditions
        .map((c) => `${c.column} ${c.operator} ?`)
        .join(" AND ");
      query += ` WHERE ${where}`;
    }
 
    if (this.orderByClause) {
      query += ` ORDER BY ${this.orderByClause.column} ${this.orderByClause.direction}`;
    }
 
    if (this.limitValue) {
      query += ` LIMIT ${this.limitValue}`;
    }
 
    return {
      sql: query,
      params: this.conditions.map((c) => c.value),
    };
  }
}
 
// Usage
const query = new QueryBuilder("users")
  .select("id", "name", "email")
  .where("status", "=", "active")
  .where("age", ">=", 18)
  .orderBy("created_at", "DESC")
  .limit(10)
  .build();
 
// { sql: 'SELECT id, name, email FROM users WHERE status = ? AND age >= ? ORDER BY created_at DESC LIMIT 10',
//   params: ['active', 18] }

Singleton Pattern

What is it? A pattern that ensures a class has only one instance and provides a global access point to it.

Why do we need it? Some things should only exist once:

  • Database connection pool: Creating multiple pools wastes resources
  • Configuration manager: One source of truth for settings
  • Logger: Consistent logging across the application
  • Cache: Shared cache avoids duplicate data

The risk: Singletons are essentially global state, which can make testing and debugging harder. Use sparingly.

Module Caching = Natural Singleton

In Node.js, modules are cached. Exporting an instance creates a singleton:

JAVASCRIPT
// database.js
class Database {
  constructor() {
    this.connection = null;
  }
 
  connect(url) {
    this.connection = createConnection(url);
  }
 
  query(sql) {
    return this.connection.execute(sql);
  }
}
 
module.exports = new Database(); // Same instance everywhere
 
// anywhere.js
const db = require("./database");
db.connect("postgres://localhost/mydb");
 
// elsewhere.js
const db = require("./database"); // Same instance!
db.query("SELECT * FROM users"); // Uses the connection from anywhere.js

Explicit Singleton

When you need more control:

JAVASCRIPT
class Logger {
  static instance = null;
 
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
 
    this.logs = [];
    Logger.instance = this;
  }
 
  log(message) {
    this.logs.push({ message, timestamp: Date.now() });
    console.log(message);
  }
 
  static getInstance() {
    if (!Logger.instance) {
      Logger.instance = new Logger();
    }
    return Logger.instance;
  }
}
 
// Both return the same instance
const logger1 = new Logger();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true

Warning

Singletons can make testing difficult because they maintain state across tests. Consider dependency injection as an alternative.

Lazy Initialization

Defer creation until first access:

JAVASCRIPT
let instance = null;
 
function getDatabase() {
  if (!instance) {
    instance = new Database();
    instance.connect(process.env.DATABASE_URL);
  }
  return instance;
}
 
module.exports = { getDatabase };
 
// Usage
const { getDatabase } = require("./database");
const db = getDatabase(); // Created on first call

Dependency Injection

What is it? A pattern where an object receives its dependencies from external sources rather than creating them itself.

Why do we need it?

  1. Testability: Inject mock dependencies in tests
  2. Flexibility: Swap implementations without changing code
  3. Decoupling: Components don't need to know how to create dependencies
  4. Configuration: Wire dependencies differently per environment

The key insight: Instead of new Database() inside your class, accept the database as a parameter.

PLAINTEXT
Without DI:                      With DI:
┌────────────────┐                 ┌────────────────┐
│  UserService   │                 │    Container   │
│                │                 │                │
│  new Database()│                 │  db, logger,   │
│  new Logger()  │                 │  email, etc    │
│  new Email()   │                 └───────┬────────┘
└────────────────┘                         │
     │                               │ inject
     │ tightly coupled               ▼
     ▼                         ┌────────────────┐
  Hard to test!               │  UserService   │
  Hard to change!             │  (uses injected│
                              │   dependencies)│
                              └────────────────┘

Without DI (Tightly Coupled)

JAVASCRIPT
// user-service.js
const Database = require("./database");
const Logger = require("./logger");
const EmailService = require("./email");
 
class UserService {
  constructor() {
    this.db = new Database(); // Hard to test
    this.logger = new Logger(); // Can't swap implementations
    this.email = new EmailService();
  }
 
  async createUser(data) {
    const user = await this.db.insert("users", data);
    this.logger.log(`User created: ${user.id}`);
    await this.email.send(user.email, "Welcome!");
    return user;
  }
}

With DI (Loosely Coupled)

JAVASCRIPT
// user-service.js
class UserService {
  constructor({ db, logger, email }) {
    this.db = db;
    this.logger = logger;
    this.email = email;
  }
 
  async createUser(data) {
    const user = await this.db.insert("users", data);
    this.logger.log(`User created: ${user.id}`);
    await this.email.send(user.email, "Welcome!");
    return user;
  }
}
 
module.exports = UserService;
 
// production.js
const UserService = require("./user-service");
const db = require("./database");
const logger = require("./logger");
const email = require("./email");
 
const userService = new UserService({ db, logger, email });
 
// test.js
const UserService = require("./user-service");
 
const mockDb = { insert: jest.fn() };
const mockLogger = { log: jest.fn() };
const mockEmail = { send: jest.fn() };
 
const userService = new UserService({
  db: mockDb,
  logger: mockLogger,
  email: mockEmail,
});
 
// Now you can test UserService in isolation!

DI Container

For larger applications, use a DI container:

JAVASCRIPT
class Container {
  constructor() {
    this.services = new Map();
    this.singletons = new Map();
  }
 
  register(name, factory, options = {}) {
    this.services.set(name, { factory, ...options });
  }
 
  resolve(name) {
    const service = this.services.get(name);
    if (!service) {
      throw new Error(`Service not found: ${name}`);
    }
 
    if (service.singleton) {
      if (!this.singletons.has(name)) {
        this.singletons.set(name, service.factory(this));
      }
      return this.singletons.get(name);
    }
 
    return service.factory(this);
  }
}
 
// Setup
const container = new Container();
 
container.register(
  "config",
  () => ({
    dbUrl: process.env.DATABASE_URL,
    logLevel: process.env.LOG_LEVEL,
  }),
  { singleton: true },
);
 
container.register(
  "logger",
  (c) => {
    const config = c.resolve("config");
    return new Logger(config.logLevel);
  },
  { singleton: true },
);
 
container.register(
  "database",
  (c) => {
    const config = c.resolve("config");
    const logger = c.resolve("logger");
    return new Database(config.dbUrl, logger);
  },
  { singleton: true },
);
 
container.register("userService", (c) => {
  return new UserService({
    db: c.resolve("database"),
    logger: c.resolve("logger"),
  });
});
 
// Usage
const userService = container.resolve("userService");

Real-World DI with Awilix

JAVASCRIPT
const { createContainer, asClass, asValue, asFunction } = require("awilix");
 
// Create container
const container = createContainer();
 
// Register services
container.register({
  // Values
  config: asValue({
    dbUrl: process.env.DATABASE_URL,
    port: process.env.PORT,
  }),
 
  // Classes (auto-resolve constructor params)
  logger: asClass(Logger).singleton(),
  database: asClass(Database).singleton(),
  userService: asClass(UserService).scoped(),
  userController: asClass(UserController).scoped(),
});
 
// Resolve
const userService = container.resolve("userService");
 
// With Express
const express = require("express");
const { scopePerRequest } = require("awilix-express");
 
const app = express();
app.use(scopePerRequest(container));
 
app.get("/users", (req, res) => {
  // Each request gets its own scoped container
  const userController = req.container.resolve("userController");
  return userController.list(req, res);
});

Revealing Constructor Pattern

What is it? A pattern where the constructor exposes private functionality via callback arguments, but only during object construction.

Why do we need it? Sometimes you want to:

  • Allow setup of internal state during construction only
  • Expose mutation methods temporarily, then make the object immutable
  • Provide privileged access that disappears after initialization

The classic example is Promise: The resolve and reject functions are only available inside the executor callback. After construction, there's no way to settle the Promise from outside.

PLAINTEXT
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502               CONSTRUCTION PHASE                    \u2502
\u2502  new Promise((resolve, reject) => {                \u2502
\u2502       \u2502            \u2502                               \u2502
\u2502       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510       \u2502
\u2502       Private functions exposed temporarily  \u2502       \u2502
\u2502  })                                                  \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
                            \u2502
                            \u25bc
\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
\u2502               AFTER CONSTRUCTION                     \u2502
\u2502   promise.then(...)  // Can observe                 \u2502
\u2502   // But resolve/reject are gone - can't mutate!   \u2502
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518

Implementation Example

JAVASCRIPT
class ImmutableBuffer {
  constructor(executor) {
    const buffer = [];
 
    // Temporary mutator - only available during construction
    const push = (item) => buffer.push(item);
 
    // Execute the setup
    executor(push);
 
    // Now make it immutable
    this.content = Object.freeze([...buffer]);
  }
 
  getContent() {
    return this.content;
  }
}
 
// Usage
const buf = new ImmutableBuffer((push) => {
  push(1);
  push(2);
  push(3);
});
 
console.log(buf.getContent()); // [1, 2, 3]
// No way to modify after construction!

Note

This is how Promise works! The resolve and reject functions are only available inside the executor, maintaining encapsulation.


Summary

Creational patterns manage object creation:

PatternWhen to Use
FactoryEncapsulate creation logic, decouple from concrete classes
BuilderComplex objects with many optional parameters
SingletonSingle shared instance needed globally
Dependency InjectionDecouple components, enable testing
Revealing ConstructorOne-time setup with temporary access

Key takeaways:

  1. Prefer composition over inheritance in JavaScript
  2. Module caching provides natural singletons
  3. DI makes testing easier and code more maintainable
  4. Factories hide complexity behind simple interfaces
  5. Builders create readable construction of complex objects

Warning

Don't over-engineer! Simple object literals and functions are often sufficient in JavaScript. Use patterns when they solve a real problem, not just because they're "best practice."