Creational 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
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?
- Decouple creation from usage: Callers don't need to know concrete classes
- Centralize object construction: One place to add logging, validation, caching
- Enable dynamic instantiation: Create objects based on runtime conditions
- Hide complexity: Complex construction logic stays in the factory
Database Connection Factory
// 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:
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` directlyBuilder Pattern
The Builder pattern separates complex object construction from its representation. Useful when objects have many optional parameters.
The Problem
// 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
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:
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
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:
// 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.jsExplicit Singleton
When you need more control:
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); // trueWarning
Singletons can make testing difficult because they maintain state across tests. Consider dependency injection as an alternative.
Lazy Initialization
Defer creation until first access:
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 callDependency 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?
- Testability: Inject mock dependencies in tests
- Flexibility: Swap implementations without changing code
- Decoupling: Components don't need to know how to create dependencies
- Configuration: Wire dependencies differently per environment
The key insight: Instead of new Database() inside your class, accept the database as a parameter.
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)
// 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)
// 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:
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
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.
\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\u2518Implementation Example
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:
| Pattern | When to Use |
|---|---|
| Factory | Encapsulate creation logic, decouple from concrete classes |
| Builder | Complex objects with many optional parameters |
| Singleton | Single shared instance needed globally |
| Dependency Injection | Decouple components, enable testing |
| Revealing Constructor | One-time setup with temporary access |
Key takeaways:
- Prefer composition over inheritance in JavaScript
- Module caching provides natural singletons
- DI makes testing easier and code more maintainable
- Factories hide complexity behind simple interfaces
- 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."