Every time you write new Something() scattered across your codebase, you’re making a decision about object creation — whether you realize it or not. Creational patterns make those decisions explicit. Here are 8 patterns that cover how objects get born.
1. Singleton
One instance. That’s it. The whole system shares it, and nobody gets to make a second one. I reach for this when I need a single background process that monitors or handles something globally.
public class Singleton {
private constructor() {} // Private constructor so other classes cannot create new instances
// This is the simplest approach but the downside is the instance is created even if it may never be used
// private static instance: Singleton = new Singleton();
// public static getInstance() : Singleton {
// return Singleton.instance;
// }
// This approach only creates the instance when a class calls for it
private static instance: Singleton;
public static getInstance() : Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
// Method
public helloWorld() {
console.log("Hello World");
}
}
Singleton.getInstance().helloWorld(); // Result: "Hello World"
Note
JavaScript is single-threaded, so you don’t have to worry about two classes requesting the instance at the exact same time and accidentally spawning two of them. One less headache compared to languages like Java or C#.
2. Factory
Gives you objects on demand without you having to deal with the messy details of creating them. I use this mostly when I have a bunch of similar classes (usually implementing the same interface) and I want one clean entry point to get the right one.
2.1. Purpose
- Gives you a new way to create objects
- Hides creation logic (super handy when you’re writing a library for others)
- Reduces coupling: if any class extends CarFactory, adding a new car type only means changes from CarFactory upward
Think of it like a car showroom. Without the Factory pattern, the showroom pulls one of every car model from the warehouse just to show the customer one. That’s wasteful. With the Factory pattern, you only build what the customer actually asks for.
- Car models:
interface Car {
viewCar(): void;
}
class Honda implements Car {
viewCar() {
console.log("Honda-chan");
}
}
class Nexus implements Car {
viewCar() {
console.log("Nexus-chan");
}
}
class Toyota implements Car {
viewCar() {
console.log("Toyota-chan");
}
}
- Normal approach: Prepare every car type upfront
class CarFactory {
public honda : Honda = new Honda();
public nexus : Nexus = new Nexus();
public toyota : Toyota = new Toyota();
}
var viewCar = new CarFactory();
car.honda.viewCar(); // Result: "Honda-chan"
- Factory pattern: Only prepare what the customer asked for
class CarFactory {
public car!: Car;
constructor(carType : string) {
switch(carType) {
case "HONDA": {
this.car = new Honda();
break;
}
case "NEXUS": {
this.car = new Nexus();
break;
}
case "TOYOTA": {
this.car = new Toyota();
}
}
}
}
var car = new CarFactory("TOYOTA");
car.car.viewCar();
3. Factory Method
Factory Method solves a specific problem: you need to create objects, but you don’t want the calling code to know which concrete class gets instantiated. I find this really useful when building extensible systems.
- The whole point is to hide which class is being created behind the scenes.
- Instead of calling
newdirectly, you define a method that handles creation. Subclasses override that method to swap in different types. Clean and flexible.
3.1. Structure
The Product interface gets the most references in the diagram, so that’s where I start defining things. I like to think of the diagram in two halves: the top half is the contract between two managers, and the bottom half is their employees actually talking to each other through the terms of that contract.
- Define Product
interface Product {
doStuff() : void;
}
- Create concrete products from the Product definition
class ConcreteProduct1 implements Product {
doStuff() {
return "Result of the ConcreteProduct1";
}
}
class ConcreteProduct2 implements Product {
doStuff() {
return "Result of the ConcreteProduct2";
}
}
- Define Creator
abstract class Creator {
public abstract createProduct() : Product;
someOperation() {
// Call the factory method to create a Product object.
let product : Product = this.createProduct();
// Now, use the product.
return `Creator: The same creator's code has just worked with ${product.doStuff()}`;
}
}
- Create Creator instances
class ConcreteCreator1 extends Creator {
public createProduct() {
return new ConcreteProduct1();
}
}
class ConcreteCreator2 extends Creator {
public createProduct() {
return new ConcreteProduct2();
}
}
- Client usage
function clientCode(creator: Creator) {
console.log(creator.someOperation());
}
console.log("App: Launched with the ConcreteCreator2\n");
clientCode(new ConcreteCreator2());
- Output
App: Launched with the ConcreteCreator2
The same creator's code has just worked with Result of the ConcreteProduct2
4. Abstract Factory
Abstract Factory wraps up a bunch of related, complex classes behind a single object. You’ve got classes that share some structure — maybe they all descend from the same abstract class — and you bundle the creation logic into one place. Everything happens inside that factory, and the consumer just gets back what they need. I think of it as the “family of products” pattern.
Usage frequency: Pretty common.
4.1. Structure
- First, define what products you want to create
interface AbstractProductA {
functionA(): string;
}
interface AbstractProductB {
functionB(): string;
}
- Create a factory definition that produces both A and B
interface AbstractFactory {
createProductA(): AbstractProductA;
createProductB(): AbstractProductB;
}
- We want 2 factories making 2 types of products (A and B), but in different versions. Factory 1 makes version 1, factory 2 makes version 2. But before we build the factories, we need templates for the products so the factory knows what to produce.
class ConcreteProductA1 implements AbstractProductA {
functionA(): string {
return "The result of the product A1.";
}
}
class ConcreteProductA2 implements AbstractProductA {
functionA(): string {
return "The result of the product A2.";
}
}
class ConcreteProductB1 implements AbstractProductB {
functionB(): string {
return "The result of the product B1.";
}
}
class ConcreteProductB2 implements AbstractProductB {
functionB(): string {
return "The result of the product B2.";
}
}
- Now we build the actual factories
class ConcreteFactory1 implements AbstractFactory {
createProductA(): AbstractProductA {
return new ConcreteProductA1();
}
createProductB(): AbstractProductB {
return new ConcreteProductB1();
}
}
class ConcreteFactory2 implements AbstractFactory {
createProductA(): AbstractProductA {
return new ConcreteProductA2();
}
createProductB(): AbstractProductB {
return new ConcreteProductB2();
}
}
- Client usage
function clientCode(factory: AbstractFactory) {
let product_a = factory.createProductA();
let product_b = factory.createProductB();
console.log(product_b.functionB());
}
console.log("Client: Testing client code with the first factory type");
clientCode(new ConcreteFactory1());
console.log("Client: Testing the same client code with the second factory type");
clientCode(new ConcreteFactory2());
- Output
Client: Testing client code with the first factory type
The result of the product B1.
Client: Testing the same client code with the second factory type
The result of the product B2.
Takeaways:
- You have to define a product before you can create it.
- You have to define and template the products before you can define and build the factory.
- The core idea is layering: define things, combine them, reduce coupling. Adding a new product means adding code, not rewriting existing code. That’s the whole game.
4.2. Differentiating Abstract Factory and Factory Method
4.2.1. Similarities
- Both are Factory Patterns
- Both reduce coupling between your program and specific implementations
- Both encapsulate object creation so your code stays independent of concrete types
4.2.2. Differences
Factory Method
- Uses classes to create products.
- Relies on inheritance. You extend a class, override the factory method, and that method produces the object.
- The idea: use subclasses to generate the desired object. The consumer only knows about the abstract class (like “poultry”), and the subclasses handle specifics (chicken, duck, goose). Your program stays independent of those concrete types.
- Adding a new product to the group? Just one method.
- The consumer never knows what’s being created — they just call the method and get their object.
Abstract Factory
- Uses objects (not classes) to create products.
- Relies on composition instead of inheritance.
- You create an abstract type for producing a whole family of products. Subclasses of that abstract type decide how to create each product. You instantiate one of those subclasses (that instance is your factory) and inject it where needed. Just like Factory Method, the consuming code stays decoupled from concrete products. Bonus: related products are already grouped together. The downside? Adding a new product to the family means modifying every factory subclass. People really hate Abstract Factory for that.
- Can create many different product types.
- Abstract Factory often uses multiple Factory Methods internally. The factory subclasses use Factory Methods to create their respective products. In that case, Factory Methods are used purely for product creation.
4.3. When to use Abstract Factory vs Factory Method?
-
Abstract Factory: When you need to create multiple product types at once and you want consumers completely shielded from concrete classes.
-
Factory Method: When you need to create just one product type, or you want your program decoupled from the concrete classes, or you don’t know yet what subclasses you’ll need. Create a subclass (a factory implementing an abstract type) and implement the Factory Method.
Source: Head First - Design Patterns book
5. Builder
Builder lets you construct complex objects step by step, using simple building blocks. Each step is independent. I reach for this whenever I see a constructor with too many parameters — that “telescoping constructor” problem where you keep adding parameters and it gets unreadable.
- Lets you create complex objects through simple, readable statements that set properties one at a time.
- Kills the telescoping constructor anti-pattern. You know the one — a class with tons of properties and constructors that keep growing.
- Great when the algorithm for setting each property doesn’t depend on the others.
5.1. Advantages
- Gives you another clean way to initialize objects
- No more writing a dozen constructors
5.2. Limitations
- You need a separate builder for each class. That’s extra code.
Usage frequency: Common
5.3. Main Components
- Product: The complex object you’re building, with lots of properties
- Builder: An abstract class or interface declaring the methods for building
- ConcreteBuilder: Implements Builder with the actual construction details. It holds the instances it creates and provides a method to retrieve them
- Director: The thing that calls Builder to orchestrate the build
5.4. Structure
interface Builder gets the most references in the diagram, so I start there. But first you need the Product definition — you can’t build a builder without knowing what it’s building.
- Define the product
class Product {
constructor(
private partA: string,
private partB: string,
private partC: string
) { }
show() : string {
return `This product has 3 parts: ${this.partA}, ${this.partB} and ${this.partC}`;
}
}
- Define Builder
abstract class Builder {
abstract BuildPartA(content: string): Builder;
abstract BuildPartB(content: string): Builder;
abstract BuildPartC(content: string): Builder;
abstract GetResult(): Product;
}
- Create a concrete builder for producing a specific product type
class ConcreteBuilder1 extends Builder {
private partA! : string;
private partB! : string;
private partC! : string;
BuildPartA(content: string) : Builder {
this.partA = content;
return this; // Used for method chaining when client uses it
}
BuildPartB(content: string) : Builder {
this.partB = content;
return this;
}
BuildPartC(content: string) : Builder {
this.partC = content;
return this;
}
GetResult() : Product {
return new Product(this.partA, this.partB, this.partC);
}
}
- Create a Director that uses Builder
class Director {
private product! : Product;
constructor(private builder : Builder) {
this.product = this.builder.GetResult();
}
showProduct() {
return this.product.show();
}
}
- Client usage
let b1 : Builder = new ConcreteBuilder1();
let director : Director = new Director(
b1.BuildPartA('Ngô')
.BuildPartB('Quang')
.BuildPartC('Sang') // Method chaining
);
console.log(director.showProduct());
- Output
This product has 3 parts: Ngô, Quang and Sang
6. Object Pool
Object Pool manages a cache of reusable objects. Instead of creating something new every time, clients just grab an existing one from the pool. The pool either auto-creates objects when empty or caps out at a fixed number. I’ve seen this pattern a lot in connection pooling for databases.
6.1. Example
Think of it like an office supply warehouse. New hire shows up, the manager checks the warehouse for spare equipment. Got some? Great, grab it. Don’t have any? Order new stuff. Employee gets fired? Their gear goes back to the warehouse for the next person.
This analogy also highlights the problems. First, the pool itself costs resources — you need a warehouse (space, money, someone to manage it). Second, stuff gets stale. You need an iPhone for a new employee, but the warehouse only has a Nokia 1080. RIP. You’re paying for storage you can’t even use.
The general idea: if instances of a class can be reused, reuse them instead of creating new ones.
6.2. Advantages
- Better application performance
- Really effective when object initialization is expensive
- Manages connections with built-in reuse and sharing
- Can cap the maximum number of objects
6.3. Disadvantages
- Can generate garbage. You’ll need periodic cleanup.
- You have to be careful about object state. When something returns to the pool, it must be in a valid, clean state. Otherwise clients get objects in unexpected states, which leads to bugs, inconsistencies, or information leaks.
6.4. When to use Object Pool
- Object creation is expensive (database queries, network connections)
- You need lots of objects in a short time (memory fragmentation risk)
- You’re constantly creating and destroying objects
- You want controlled reuse instead of uncontrolled
newcalls - Multiple clients need the same resource at different times
6.5. Structure
- Reusable: The objects that get recycled
- Client: The classes that use reusable objects
- ReusablePool: The manager that holds reusable objects and hands them out to clients
6.6. Object Pool Example via a Taxi Application
Picture a taxi company with only N cars. The company tracks which cars are free vs. carrying passengers, dispatches idle cars to new customers, makes customers wait if all cars are busy, and cancels if the wait gets too long.
Here’s how I modeled it:
- Taxi: Represents one taxi with its properties and methods.
- TaxiPool: Represents the company.
- getTaxi(): Grabs an idle taxi. Throws if the wait is too long.
- release(): Returns a taxi to the pool after the ride.
- available: List of idle taxis.
- inUse: List of taxis currently on a ride.
- ClientThread: Represents a customer — calls for a taxi, rides, gets dropped off.
In the code below, I simulate a pool of 4 taxis with 8 simultaneous customer calls. Each taxi takes 200ms to arrive, carries passengers for 1000-1500ms (random), and each customer waits a max of 1200ms before giving up.
- Declare the Taxi class
class Taxi {
constructor(private name: string) {}
getName() : String {
return this.name;
}
setName(name: string) {
this.name = name;
}
toString() : String {
return `Taxi [name = ${this.name}]`
}
}
- Declare TaxiPool as the object pool
class TaxiPool {
private readonly EXPIRED_TIME_IN_MILISECOND: number = 1200;
private readonly NUMBER_OF_TAXI: number = 4;
private available: Array<Taxi> = new Array<Taxi>();
private inUse: Array<Taxi> = new Array<Taxi>();
private count: number = 0;
private waiting: boolean = false;
async getTaxi(): Promise<Taxi> {
if (this.available.length > 0) {
let taxi: Taxi = this.available.shift() || new Taxi('');
this.inUse.push(taxi);
return Promise.resolve(taxi);
}
if (this.count == this.NUMBER_OF_TAXI) {
this.waitingUntilTaxiAvailable();
return this.getTaxi();
}
return await this.createTaxi();
}
release(taxi: Taxi) {
this.inUse.splice(this.inUse.indexOf(taxi), 1);
this.available.push(taxi);
console.log(taxi.getName() + " is free");
}
async createTaxi(): Promise<Taxi> {
await this.sleep(200);
this.count++;
let taxi: Taxi = new Taxi("Taxi " + this.count);
console.log(taxi.getName() + " is created");
return taxi;
}
async waitingUntilTaxiAvailable(): Promise<void> {
if (this.waiting) {
this.waiting = false;
throw new Error("No taxi available");
}
this.waiting = true;
await this.waitingF(this.EXPIRED_TIME_IN_MILISECOND);
}
async waitingF(numberOfSecond: number) {
try {
await this.sleep(numberOfSecond);
} catch (e) {
throw new Error(e);
}
}
sleep(ms: any): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
- Create ClientThread to call a taxi
class ClientThread {
private clientNumber: number = Math.floor(Math.random() * 501) + 1000;
constructor(private taxiPool: TaxiPool) { }
run(): void {
this.takeATaxi();
}
private async takeATaxi(): Promise<void> {
try {
console.log("New client: " + this.clientNumber);
let taxi: Taxi;
taxi = await this.taxiPool.getTaxi().then(res => res);
await this.sleep(Math.floor(Math.random() * 501) + 1000);
this.taxiPool.release(taxi);
console.log("Served the client: " + this.clientNumber);
} catch (e) {
console.log(">>>Rejected the client: " + this.clientNumber);
}
}
sleep(ms: any): Promise<any> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
- Run the program
const NUM_OF_CLIENT: number = 8;
let taxiPool: TaxiPool = new TaxiPool();
for (let i = 0; i < NUM_OF_CLIENT; i++) {
let client: ClientThread = new ClientThread(taxiPool);
client.run();
}
- Result
New client: 1196
New client: 1346
New client: 1398
New client: 1052
New client: 1132
New client: 1280
New client: 1174
New client: 1247
Taxi 1 is created
Taxi 2 is created
Taxi 3 is created
Taxi 4 is created
Taxi 5 is created
Taxi 6 is created
Taxi 7 is created
Taxi 8 is created
Taxi 8 is free
Served the client: 1247
Taxi 5 is free
Served the client: 1132
Taxi 6 is free
Served the client: 1280
Taxi 3 is free
Served the client: 1398
Taxi 4 is free
Served the client: 1052
Taxi 2 is free
Served the client: 1346
Taxi 7 is free
Served the client: 1174
Taxi 1 is free
Served the client: 1196
Observations
- The big win with pooling is reusing already-allocated resources. With 4 taxis, you can often serve more than 4 requests at a time because taxis get recycled. No wasted initialization time, no wasted memory from constantly creating and destroying objects.
- You can make it smarter with N (minimum instances during idle time) and M (maximum instances your hardware can handle). After peak demand passes, the pool shrinks back down.
Reference: https://gpcoder.com/4456-huong-dan-java-design-pattern-object-pool/
7. Prototype
Prototype creates new objects by copying an existing one. Instead of building from scratch, you clone a template. I use this a lot more than I expected to.
Usage frequency: High
7.1. Structure
7.2. Participating Components
- Prototype - CustomerPrototype: Provides an interface to clone itself
- Clones - Customer: Objects created by cloning
class CustomerPrototype {
constructor(public proto: any) { }
clone() {
var customer: Customer = new Customer(
this.proto.first,
this.proto.last,
this.proto.status
);
return customer;
};
}
class Customer {
constructor(
public first: string,
public last: string,
public status: string
) { }
say() {
console.log(
`name: ${this.first} ${this.last}, status: ${this.status}`
);
}
}
- Run the program
let proto = new Customer("n/a", "n/a", "pending");
let prototype = new CustomerPrototype(proto);
let customer = prototype.clone();
customer.say();
- Result
name: n/a n/a, status: pending
- Quick clone in ES6
let prototye : {
name: "John"
}
let custom = {...prototype};
Here’s something that tripped me up early on. Every instance inherits properties and methods from its class, which means each instance eats memory for its own copy. Prototype Pattern has a variant that solves this by sharing properties across instances.
- Say you’re writing a game and you want a Warrior class:
function Warrior(name) {
this.name = name;
this.hp = 100;
this.bash = function (target) {
target.hp -= 10;
};
this.slash = function (target) {
target.hp /= 2;
};
}
let harryPotter = new Warrior("Harry Potter");
let ngan = new Warrior("Ngan");
let snake = new Warrior("Snake");
harryPotter.bash(ngan);
console.log(ngan.hp); // 90
ngan.slash(snake);
console.log(snake.hp); // 50
- Three warriors, all sharing the same constructor:
console.log(
harryPotter.constructor === ngan.constructor,
snake.constructor === ngan.constructor
); // Result: true true
- They can fight each other just fine. But here’s the problem — each warrior has its own copy of the skill functions, even though the functions are identical. That’s wasteful:
console.log(harryPotter.bash === ngan.bash, snake.bash === ngan.bash); // Result: false false
- This is exactly why
prototypeexists. Move the shared methods there:
function Warrior(name) {
this.name = name;
this.hp = 100;
}
Warrior.prototype.bash = function (target) {
target.hp -= 10;
};
Warrior.prototype.slash = function (target) {
target.hp /= 2;
};
let harryPotter = new Warrior("Harry Potter");
let ngan = new Warrior("Ngan");
let snake = new Warrior("Snake");
console.log(harryPotter.bash === ngan.bash, snake.bash === ngan.bash); // Result: true, true
- Same thing written differently:
Warrior.prototype = {
bash: function (target) {
target.hp -= 10;
},
slash: function (target) {
target.hp /= 2;
},
};
Watch out though — this approach replaces the entire prototype object, which means you lose the original Warrior constructor reference. But it shows exactly how prototype-based inheritance and sharing works in JavaScript.
8. Dependency Injection
Instead of letting a class create its own dependencies internally, you declare the dependency as an interface and pass the concrete instance in from outside. This makes everything more flexible and testable.
8.1. Advantages
- Unit testing becomes straightforward
- Swapping implementations is easy
- Scales well as the project grows
8.2. Problem Statement
Say you have a motorcycle. It has parts, like tires. There are different tire types at different prices — type A at $1000, type B at $500. You picked type B to save money.
class LopA {
public price : number = 1000;
}
class LopB {
public price : number = 500;
}
class Xe {
public lop : LopA = new LopA();
}
When you create a motorcycle object, the class internally creates a tire instance. That hard dependency causes two problems:
- You can’t swap the component. Got money now and want type A tires? Too bad, it’s baked in.
- Changing anything inside forces you to modify the class itself. That violates the Open-Closed principle in SOLID.
8.3. Solution
DI fixes this cleanly. The key insight: declare your dependency as an interface type. Don’t hard-wire it to a class. Don’t create instances inside the constructor. Instead, create the instance outside and pass it in. This kills the tight coupling.
Back to the example — instead of pre-creating a tire inside Xe, just declare the interface type and inject the concrete instance from outside:
interface Lop {
price: number;
}
class LopA implements Lop {
public price : number = 1000;
}
class LopB implements Lop {
public price : number = 500;
}
class Xe {
constructor(public lop: Lop) {}
}
// run
let xe : Xe = new Xe(new LopB());
console.log(xe.lop.price); // Result: 500
Want to upgrade from type B to type A? Just do this:
xe.lop = new LopA();
console.log(xe.lop.price); // Result: 1000
Switched tires without touching the Xe class at all. That’s SOLID in action.
8.4. Types of DI
DI comes in two flavors:
- Constructor Injection (the example above)
- Setter Injection
class Xe {
public _lop!: Lop;
set lop(lop: Lop) {
this._lop = lop;
}
}
let xe : Xe = new Xe();
xe.lop = new LopB();