Chapter 7: Creational Design Patterns
Summary
Creational design patterns deal with how objects are created. In JavaScript and Node.js, the dynamic type system and first-class functions unlock patterns that are simpler and more flexible than their classical GoF counterparts.
This chapter covers five patterns: the Factory (createImage switching between ImageJpeg/ImageGif/ImagePng by extension, createPerson with closures for encapsulation, createProfiler returning real vs no-op by environment), the Builder (BoatBuilder with withMotors/withSails/withCabin/hullColor/build, UrlBuilder, superagent as real-world example), the Revealing Constructor (ImmutableBuffer exposing write only during construction, Promise as the canonical example), the Singleton (module cache makes every module a singleton), and Dependency Injection for wiring modules without tight coupling.
Key Concepts
The Factory Pattern
Core Idea A factory is a function that creates and returns objects. The caller doesn't know (or care) what concrete class or structure is returned -- only that it satisfies an expected interface. Duck typing makes this work in JavaScript.
The createImage Factory
// Factory decides which class based on file extension
function createImage(name) {
if (name.match(/\.jpe?g$/)) {
return new ImageJpeg(name)
} else if (name.match(/\.gif$/)) {
return new ImageGif(name)
} else if (name.match(/\.png$/)) {
return new ImagePng(name)
} else {
throw new Error('Unsupported format')
}
}
// Consumer doesn't know or care about the concrete class
const image = createImage('photo.jpg') // ImageJpeg instance
image.render() // works via duck typing
Encapsulation via Closures (createPerson)
In JavaScript, factories can enforce true encapsulation using closures -- no
this, no prototype, no way to access private state:
function createPerson(name, age) {
// Private state -- only accessible via closures
const privateName = name
const privateAge = age
return {
getName() { return privateName },
getAge() { return privateAge },
toString() { return `${privateName} (${privateAge})` }
}
}
const person = createPerson('Alice', 30)
person.getName() // 'Alice'
person.privateName // undefined -- truly private
The Code Profiler Example
The factory returns different implementations based on the environment:
function createProfiler(label) {
if (process.env.NODE_ENV === 'production') {
// No-op profiler -- same interface, zero overhead
return { start() {}, end() {} }
}
// Real profiler for development
let startTime
return {
start() { startTime = process.hrtime.bigint() },
end() {
const diff = process.hrtime.bigint() - startTime
console.log(`${label}: ${diff}ns`)
}
}
}
// Consumer code is identical in both environments
const profiler = createProfiler('database query')
profiler.start()
// ... do work ...
profiler.end()
Duck Typing
'If it walks like a duck and quacks like a duck, it's a duck.' The consumer
calls profiler.start() and profiler.end() -- it doesn't matter whether
the object is a real profiler or a no-op. Same interface, different behavior.
Knex uses this same approach: its factory returns different database adapters
based on configuration.
The Builder Pattern
Core Idea Separate the construction of a complex object from its representation. Use a fluent API with chainable methods that each configure one aspect of the object.
The Builder solves the telescoping constructor problem:
// Bad -- Boat has a complex constructor with many params
const boat = new Boat(true, false, 2, 'Yamaha', true, 'blue')
// What do these positional args mean??
// Good -- BoatBuilder with named steps
const boat = new BoatBuilder()
.withMotors(2, 'Yamaha')
.withSails(0, null)
.withCabin()
.hullColor('blue')
.build()
Implementing the BoatBuilder
class BoatBuilder {
withMotors(count, brand) {
this.hasMotor = true
this.motorCount = count
this.motorBrand = brand
return this // enables chaining
}
withSails(count, material) {
this.hasSails = true
this.sailCount = count
this.sailMaterial = material
return this
}
withCabin() {
this.hasCabin = true
return this
}
hullColor(color) {
this.color = color
return this
}
build() {
return new Boat({
hasMotor: this.hasMotor,
motorCount: this.motorCount,
motorBrand: this.motorBrand,
hasSails: this.hasSails,
sailCount: this.sailCount,
sailMaterial: this.sailMaterial,
hasCabin: this.hasCabin,
color: this.color
})
}
}
The UrlBuilder Example
class UrlBuilder {
setProtocol(protocol) { this.protocol = protocol; return this }
setHostname(hostname) { this.hostname = hostname; return this }
setPort(port) { this.port = port; return this }
setPathname(pathname) { this.pathname = pathname; return this }
setSearch(search) { this.search = search; return this }
setHash(hash) { this.hash = hash; return this }
build() {
return new URL(
`${this.protocol}://${this.hostname}` +
`${this.port ? ':' + this.port : ''}` +
`${this.pathname || ''}` +
`${this.search ? '?' + this.search : ''}` +
`${this.hash ? '#' + this.hash : ''}`
)
}
}
Real-World Builder: Superagent
superagent
.post('https://api.example.com/users')
.set('Content-Type', 'application/json')
.send({ name: 'Alice' })
.timeout(5000)
.end((err, res) => { /* ... */ })
Each method configures one aspect and returns the request for chaining.
The Revealing Constructor Pattern
Core Idea The constructor receives an executor function that gets temporary access to the object's private internals. Once construction is complete, those internals are sealed -- no further modification is possible.
This pattern is unique to JavaScript and doesn't exist in the classic GoF catalog.
Promise -- The Canonical Example
const promise = new Promise((resolve, reject) => {
// resolve and reject are PRIVATE methods, temporarily revealed
// After this executor returns, no one can call them externally
setTimeout(() => resolve('done'), 1000)
})
// Outside: you can .then() and .catch(), but you can NEVER call resolve/reject
Building an ImmutableBuffer
class ImmutableBuffer {
constructor(size, executor) {
const buffer = Buffer.alloc(size)
// Reveal the write function only during construction
const write = (data, offset = 0) => {
data.copy(buffer, offset)
}
// The executor gets temporary write access
executor(write)
// After executor completes, write is no longer accessible
this.buffer = buffer
}
toString(encoding = 'utf8') {
return this.buffer.toString(encoding)
}
}
const immutable = new ImmutableBuffer(13, (write) => {
write(Buffer.from('Hello, World!'))
})
immutable.toString() // 'Hello, World!'
// No way to modify the buffer after construction
The executor runs synchronously The executor function runs synchronously during construction. If you need async initialization, combine this with a factory function that awaits setup before returning the sealed object.
The Singleton Pattern
In Node.js, You Probably Don't Need the Traditional Singleton
The module system's require cache effectively makes every exported instance a
singleton. All modules that require('./db') get the exact same object.
How the Module Cache Creates Singletons
// db.js -- exports a single instance
class Database {
constructor() { this.connection = null }
connect(uri) { /* ... */ }
query(sql) { /* ... */ }
}
module.exports = new Database()
// app.js
const db = require('./db') // creates the instance
const db2 = require('./db') // returns SAME instance from cache
db === db2 // true
When the Cache Breaks
Duplicate Packages in node_modules
If npm installs a package at both node_modules/foo/ and
node_modules/bar/node_modules/foo/, they have different resolved paths
and different cache entries -- two separate instances, not one singleton.
// True singleton via global symbol -- survives duplicate packages
const SINGLETON_KEY = Symbol.for('app.db.instance')
function getDatabase() {
if (!global[SINGLETON_KEY]) {
global[SINGLETON_KEY] = new Database()
}
return global[SINGLETON_KEY]
}
Wiring Modules
How modules find and connect to their dependencies.
Approach 1: Hardcoded Dependencies (Singleton Pattern)
// userService.js -- tightly coupled to specific db module
const db = require('./db')
module.exports = {
findUser(id) { return db.query('SELECT * FROM users WHERE id = ?', [id]) }
}
Pros: Simple, works with module cache singleton. Cons: Hard to test (must mock the require cache), tightly coupled.
Approach 2: Dependency Injection
// userService.js -- dependencies passed in
module.exports = function createUserService(db) {
return {
findUser(id) { return db.query('SELECT * FROM users WHERE id = ?', [id]) }
}
}
// In production:
const db = require('./db')
const userService = require('./userService')(db)
// In tests:
const mockDb = { query: () => ({ id: 1, name: 'Test' }) }
const userService = require('./userService')(mockDb)
Pros: Explicit deps, easy to test, loosely coupled. Cons: More wiring code, caller must know the dependency graph.
Practical Guidance
Most Node.js apps use hardcoded requires for simplicity. Use DI when
testability matters or when you need to swap implementations. DI containers
(like awilix) automate the wiring.
Patterns Comparison
| Pattern | Purpose | Key Mechanism | Real-World Example |
|---|---|---|---|
| Factory | Decouple creation from implementation | Function returns objects based on input | createImage, Knex, Buffer.from() |
| Builder | Step-by-step complex object creation | Chainable setters + build() | BoatBuilder, Superagent |
| Revealing Constructor | Controlled construction then seal | Executor receives private methods | new Promise((resolve, reject) => {}) |
| Singleton | Single shared instance | Module cache / global symbol | Database connections, loggers |
| DI | Loose coupling & testability | Pass deps as parameters | Express middleware, test mocks |
Mind Map
Connections
- Previous: Chapter 6 -- Coding with Streams
- Next: Chapter 8 -- Structural Design Patterns
- The Factory pattern builds on Chapter 2's export patterns
- Singleton depends directly on the module cache explained in Chapter 2
- DI patterns apply to async modules from Chapter 5