How objects get created shapes how coupled your system is. Factory, Builder, and Singleton control object creation so the rest of your code depends on interfaces, not constructors.
Why: calling a constructor with new hard-codes a concrete class into the caller — a factory hides which class is created behind a method, so code depends on the interface and the concrete choice can vary. When: use it when creation logic is non-trivial or the exact type is decided at runtime (by config, input, or platform). Where: factories are the most common creational pattern because they directly serve the Dependency Inversion principle.
interface PaymentGateway { charge(cents: number): void }
class StripeGateway implements PaymentGateway { charge(c: number) {} }
class PayPalGateway implements PaymentGateway { charge(c: number) {} }
// The factory decides the concrete type; callers just get a PaymentGateway.
function makeGateway(provider: string): PaymentGateway {
switch (provider) {
case 'stripe': return new StripeGateway()
case 'paypal': return new PayPalGateway()
default: throw new Error('unknown provider')
}
}
// Adding a new provider touches ONLY the factory, not every call site.Why: objects with many optional parts lead to unreadable constructors with long parameter lists — a builder assembles the object step by step with named methods, making construction clear and validated. When: use it when an object has many optional fields or a multi-step, order-sensitive setup. Where: it produces immutable, valid objects and reads far better than a constructor with ten arguments.
class QueryBuilder {
private parts: string[] = []
select(cols: string) { this.parts.push(`SELECT ${cols}`); return this }
from(table: string) { this.parts.push(`FROM ${table}`); return this }
where(cond: string) { this.parts.push(`WHERE ${cond}`); return this }
build() { return this.parts.join(' ') }
}
const sql = new QueryBuilder()
.select('id, name').from('users').where('active = true')
.build() // readable, step-by-step — no 10-argument constructorWhy: a singleton guarantees one shared instance (a config, a connection pool) with global access — useful, but it is also global mutable state that hides dependencies and wrecks testability, so it is the most overused and abused pattern. When: use it rarely, for genuinely single resources; prefer dependency injection so the dependency is explicit. Where: an injected single instance gives you the "one instance" benefit without the global-state cost.
// The classic singleton — one shared instance, global access:
class Config {
private static instance: Config
private constructor(public readonly env: string) {}
static get(): Config {
if (!Config.instance) Config.instance = new Config(process.env.NODE_ENV!)
return Config.instance
}
}
// The catch: it's global mutable state -> hidden dependency, hard to test.
// Prefer INJECTING a single instance (DI) so the dependency is explicit.