After six months building the payment module for our ride-hailing platform, I realized we were creating payment processors in at least five different ways across the codebase. Some used new directly, others relied on static factory methods, and a few leaked implementation details to the UI layer. That's when I understood why we needed to formalize how objects get created. Today we're diving into creational patterns overview—the first family of Gang of Four patterns that tackle the core question: how do we control object creation?
The family: Five creational patterns and when they matter
Creational patterns solve a specific problem: they decouple the act of creating objects from the code that uses them. Let me walk through each one with the contexts where I've actually needed them.
Factory method: Subclasses decide what to create
The factory method pattern lets subclasses override a method to determine which concrete class gets instantiated. It's the lightest-weight of the five—you're not doing anything magical, just moving creation logic into a method that returns an interface.
Use when: You have one family of related types and callers shouldn't care which specific subclass they receive. I used this for payment gateways.
// Without factory method: caller knows too much
const processor = new StripeProcessor();
processor.charge(amount);
// With factory method: caller works with abstraction
abstract class PaymentProviderFactory {
abstract createProcessor(): PaymentProcessor;
process(amount: number): void {
const processor = this.createProcessor();
processor.charge(amount);
}
}
class StripeFactory extends PaymentProviderFactory {
createProcessor(): PaymentProcessor {
return new StripeProcessor();
}
}
class PayPalFactory extends PaymentProviderFactory {
createProcessor(): PaymentProcessor {
return new PayPalProcessor();
}
}The factory method is simple enough that many teams skip it entirely, using a static factory function or even direct configuration instead. That's often fine for small codebases.
Abstract factory: Families of related objects
When you need to create multiple related objects as a unit—and ensure they work together—abstract factory is your tool. It's factory method for groups of objects.
Use when: You have multiple families of related products (like "light theme components" vs "dark theme components" or "AWS infrastructure" vs "Azure infrastructure") and you want to swap entire families at once.
interface Button {
render(): string;
}
interface Input {
render(): string;
}
interface UIComponentFactory {
createButton(): Button;
createInput(): Input;
}
class LightThemeFactory implements UIComponentFactory {
createButton(): Button {
return new LightButton();
}
createInput(): Input {
return new LightInput();
}
}
class DarkThemeFactory implements UIComponentFactory {
createButton(): Button {
return new DarkButton();
}
createInput(): Input {
return new DarkInput();
}
}
// Client code never knows which theme is active
function renderForm(factory: UIComponentFactory) {
const button = factory.createButton();
const input = factory.createInput();
console.log(button.render() + input.render());
}I've found abstract factory most valuable in platforms with multiple deployment targets (cloud providers, device types, localization variants). It makes swapping entire subsystems clean.
Builder: Complex objects step by step
The builder pattern separates construction logic from representation. You build an object incrementally, often with optional fields, validation at each step, and a final immutable result.
Use when: Objects have many optional parameters, complex initialization order, or you want to express creation logic fluently. I used this extensively for API requests and domain model configuration.
class QueryBuilder {
private query: Query = {};
select(...fields: string[]): this {
this.query.fields = fields;
return this;
}
where(condition: string): this {
this.query.condition = condition;
return this;
}
limit(count: number): this {
this.query.limit = count;
return this;
}
build(): Query {
if (!this.query.fields || this.query.fields.length === 0) {
throw new Error("At least one field is required");
}
return Object.freeze(this.query);
}
}
const query = new QueryBuilder()
.select("id", "name", "email")
.where("status = 'active'")
.limit(10)
.build();In my experience, builder shines most when you're constructing domain objects or complex configurations. It also makes your code self-documenting—calling code reads almost like natural language.
Prototype: Cloning is cheaper than building
Prototype lets you create new objects by copying an existing object rather than instantiating from scratch. Useful when creation is expensive or when you want to preserve state.
Use when: Object instantiation is costly (database queries, heavy I/O, expensive computation) and you have a known good state to clone from. I've used this for caching and template objects.
interface Cloneable {
clone(): Cloneable;
}
class DatabaseConnection implements Cloneable {
private connectionString: string;
private pool: Connection[];
constructor(connectionString: string) {
this.connectionString = connectionString;
this.pool = this.initializeExpensivePool();
}
clone(): DatabaseConnection {
// Shallow copy of the object—often good enough
return Object.assign(Object.create(Object.getPrototypeOf(this)), this);
}
}
// Instead of creating new connections repeatedly
const template = new DatabaseConnection("postgres://...");
const conn1 = template.clone();
const conn2 = template.clone();Modern JavaScript's Object.assign and spread operators make prototypal cloning trivial, but you still need to think about shallow vs deep copies and state implications.
Singleton: One instance—usually via DI now
The singleton pattern restricts a class to exactly one instance and provides global access to it. It's the most controversial pattern in this family because it introduces implicit global state.
Use when: You genuinely need one and only one instance (thread-safe configuration, logging, resource pools). But honestly, I almost never write raw singletons anymore—dependency injection containers handle this better.
// Old school singleton (don't do this)
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
}
// Modern approach: DI container provides the singleton behavior
@Injectable({ scope: 'singleton' })
class Logger {
log(message: string): void {
console.log(message);
}
}The DI container approach is superior because it decouples the singleton behavior from the class itself, making the code more testable and your dependencies explicit.
Why these patterns exist
The core principle behind all creational patterns is the same: hide construction details so callers depend on interfaces, not concrete classes. This creates three major benefits.
First, it gives you flexibility. When payment requirements changed mid-project, we could add a new processor without touching code that calls process(). The factory method pattern meant we only changed one place.
Second, it centralizes validation and setup logic. Instead of having setup code scattered across your codebase, it lives in one factory. If you need to add connection pooling, configuration loading, or instrumentation, you do it once.
Third, it makes tests easier. Mocking a factory is straightforward—I can inject a test factory that returns stub processors. Without it, you're either modifying test code to use new MyMockClass() directly or passing null values around.
Modern take: Dependency injection containers handle most creation concerns
Here's what I've learned after five years building production systems: most of what creational patterns solve is now handled by DI containers in mature frameworks.
NestJS, Spring, and similar frameworks give you declarative control over object creation and lifetime without writing a single factory class. You declare what you need, and the container provides it. This approach actually implements abstract factory, singleton behavior, and factory methods all at once through annotations.
// NestJS: No manual factory needed
@Module({
providers: [
{
provide: 'PAYMENT_PROCESSOR',
useFactory: (config: ConfigService) => {
if (config.get('PAYMENT_PROVIDER') === 'stripe') {
return new StripeProcessor(config.get('STRIPE_KEY'));
}
return new PayPalProcessor(config.get('PAYPAL_KEY'));
},
inject: [ConfigService],
},
],
})
export class PaymentModule {}
// Consumer doesn't care about creation details
@Injectable()
export class OrderService {
constructor(@Inject('PAYMENT_PROCESSOR') private processor: PaymentProcessor) {}
}That said, understanding these patterns is still crucial. You'll recognize when someone's reinventing factory patterns badly. You'll know why your DI configuration looks the way it does. And for domains that don't use a container—microservices, libraries, systems programming—you'll know exactly which pattern solves your problem.
Common mistakes
- Over-engineering: Not every object needs a factory. If you're creating a value object or a simple data class, just use the constructor. Factory patterns are for when creation logic is complex or choice of implementation matters.
- Leaking implementation details: A factory method should return an interface, not a concrete class. If callers know they got a
StripeProcessor, you haven't abstracted anything. - Builder with too many validations: Put validation in the
build()method, not in each step. Otherwise your builder becomes fragile—adding a new step might break existing code that doesn't call it. - Singleton as a shortcut for "global": If you're reaching for singleton because you want something globally accessible, stop. That's a code smell. Use DI instead.
- Cloning with shared mutable state: When you prototype an object with nested objects or collections, think carefully about deep vs shallow copies. A shallow clone that shares a list with the original will bite you eventually.
Key takeaways
- Creational patterns centralize object construction logic so callers depend on interfaces, not concrete classes.
- Factory method handles single families of types; abstract factory handles coordinated families; builder handles complex step-by-step construction; prototype handles expensive cloning; singleton handles one-instance requirements.
- Modern DI containers have largely replaced manual factory implementations for application code.
- Understand these patterns so you recognize them in frameworks and know when to apply them in code that doesn't use a container.
- Don't over-engineer—use them only when creation logic is complex or implementation choice matters to callers.






Comments
Loading comments…