Design Patterns Implementation in JavaScript
Common design patterns implemented in JS/TS

A lifelong learner. Love to travel. Listen to music.
Design patterns are the battel-proof solution for common software design problems. Each pattern can be considered a generic template for a specific issue.
Design patterns are classified into these four categories,
Creational Design Pattern
Structural Design Pattern
Behavioral Design Pattern
Architectural Design Pattern
Here we will discuss a couple of common design patterns from JavaScript (or TypeScript) perspective,
Null Object
Factory [Creational]
Singleton [Creational]
Builder [Creational]
Decorator [Structure]
Facade [Structure]
Adapter [Structure]
Proxy [Structure]
Chain of Responsibility [Behavioral]
Command [Behavioral]
Observer [Behavioral]
Null Object Pattern
Consider the following examples, we have a band class. We create band objects using the name and their musical genres. Now we have a method that finds a band with name matching and then we print that particular band's genre.
It can happen in the getBand method, the name we pass as string, does not have a Band object. So it returns an undefined. In this case, when we try to read the genra from the band, it gives us an error of Cannot read properties of undefined (reading 'genre').
We have to add check in the object, if the object exists only then the genra should be printed.
class Band {
name: string;
genre: string;
constructor(name: string, genra: string) {
this.name = name;
this.genre = genra;
}
}
const bands = [
new Band('Warfaze', 'Heavy Metal'),
new Band('Artcell', 'Progressive Rock Metal')
];
const getBand = (name: string) => {
return bands.find(band => band.name === name);
}
// one way to print genre is, to check the object's existence beforehand
const myBand = getBand('Iron Maiden');
if (myBand !== undefined) {
console.log(myBand.genre);
}
// otherwise it will throw the following error
// @ts-ignore
console.log(getBand('Iron Maiden').genre); // Cannot read properties of undefined (reading 'genra')
Using Null Object Pattern we can resolve the issue using a Null object, in this case, NullBand. When no matched null object is found, we use the NullBand object.
class Band {
name: string;
genre: string;
constructor(name: string, genra: string) {
this.name = name;
this.genre = genra;
}
}
class NullBand {
name: string;
genre: string;
constructor() {
this.name = '';
this.genre = '';
}
}
const bands = [
new Band('Warfaze', 'Heavy Metal'),
new Band('Artcell', 'Progressive Rock Metal')
];
const getBand = (name: string) => {
return bands.find(band => band.name === name) ?? new NullBand;
}
console.log(getBand('Iron Maiden').genre);
Factory Pattern
Factory pattern allows creating an object using another object. It centralized the object creation logic.
For example, we have an organization where two types of employees,
Developer
Tester
We can create a developer and tester as follows,
function Developer(name) {
this.name = name;
this.jobType = 'Developer';
}
function Tester(name) {
this.name = name;
this.jobType = 'Tester';
}
const developer = new Developer('John');
const tester = new Tester('Doe');
console.log(developer); // Developer { name: 'John', jobType: 'Developer' }
console.log(tester); // Tester { name: 'Doe', jobType: 'Tester' }
Instead of using Factory Pattern we can create a factory object EmployeeFactory, which enables us to create new objects,
class Developer {
constructor(name) {
this.name = name;
this.jobType = "Developer";
}
}
class Tester {
constructor(name) {
this.name = name;
this.jobType = "Tester";
}
}
function EmployeeFactory(name, type) {
switch (type) {
case 'coding':
return new Developer(name, 'Developer');
case 'testing':
return new Tester(name, 'Tester');
}
}
[EmployeeFactory('John', 'coding'),
EmployeeFactory('Doe', 'testing')].map((employee) => console.log(employee));
Singleton Pattern
There could be some cases, where we do not want to create instances of a class more than once. Rather, we use one single instance of the class globally.
Consider we have a log manager and we collect logs from all over the application. We do want to store all these logs in a single object rather than using multiple objects. Without a singleton, each object holds its logs,
class LogManager {
constructor() {
this.logs = [];
}
insert(log) {
this.logs.push(log);
}
getLogs() {
return this.logs;
}
}
// Log from file 1
const logManager1 = new LogManager();
logManager1.insert('My Log 1');
// log from file 2
const logManager2 = new LogManager();
logManager2.insert('My Log 2');
// Want to see all the logs
console.log(logManager1.getLogs()); // ['My Log 1']
console.log(logManager2.getLogs()); // ['My Log 2']
With the singleton pattern, we ensure, the log manager does not create multiple instances. So with a single object, we can get all the logs in one place.
let logManagerInstance;
class LogManager {
constructor() {
if (logManagerInstance) {
return logManagerInstance;
}
this.logs = [];
logManagerInstance = this;
}
insert(log) {
this.logs.push(log);
}
getLogs() {
return this.logs;
}
}
// Log from file 1
const logManager1 = new LogManager();
logManager1.insert('My Log 1');
// log from file 2
const logManager2 = new LogManager();
logManager2.insert('My Log 2');
// Want to see all the logs
console.log(logManager1.getLogs()); // ["My Log 1", "My Log 2"]
console.log(logManager2.getLogs()); // ["My Log 1", "My Log 2"]
Builder Pattern
Consider the following example, where we create band information with name, lineup, genre and origin. When we only have name and origin but the lineup and genra is missing, during the creation of an object, we have to pass two undefined value.
class Band {
constructor(name, lineup, genra, origin) {
this.name = name;
this.lineup = lineup;
this.genra = genra;
this.origin = origin;
}
}
const warfaze = new Band('Warfaze', undefined, undefined, 'Dhaka');
console.log(warfaze);
With JS way, we can resolve it using JS specific builder pattern, where we only take the name but other properties will be passed as object properties. In this case, we do not have to do with undefined value.
class Band {
constructor(name, {
lineup,
genra,
origin
} = {}) {
this.name = name;
this.lineup = lineup;
this.genra = genra;
this.origin = origin;
}
}
const warfaze = new Band('Warfaze', {
origin: 'Dhaka'
});
console.log(warfaze);
We can solve this in a traditional way like the following also.
class Band {
constructor(name, lineup, genra, origin) {
this.name = name;
this.lineup = lineup;
this.genra = genra;
this.origin = origin;
}
}
class BandBuilder {
constructor(name) {
this.band = new Band(name)
}
setLineup(lineup) {
this.band.lineup = lineup;
return this;
}
setGenra(genra) {
this.band.genra = genra;
return this;
}
setOrigin(origin) {
this.band.origin = origin;
return this;
}
build() {
return this.band;
}
}
const warfaze = new BandBuilder('Warfaze')
.setOrigin('Dhaka')
.build();
console.log(warfaze);
Decorator Pattern
Decorator Pattern allows updating the behavior of the existing classes.
In the following example, we have a Vehicle method, that is used to create objects. Now, after creating a vehicle object, we can see the name, model, and price from the object.
We will use two types of decorators,
One to update the model
Another way to update the vehicle price is if
AC Sub Engineis added to the vehicle
Considerations
The base decorator takes the object in the constructor
Other decorators extend the base decorator
// Base class
class Vehicle {
constructor(name) {
this.name = name;
this.model = 'Default';
}
getPrice() {
return 2;
}
getDescription() {
return `${this.name} ${this.model}`;
}
}
// Base decorator
class VehicleDecorator {
constructor(vehicle) {
this.vehicle = vehicle;
}
getPrice() {
return this.vehicle.getPrice();
}
getDescription() {
return this.vehicle.getDescription();
}
}
// Feature decorators
class ModelDecorator extends VehicleDecorator {
constructor(vehicle, model) {
super(vehicle);
this.model = model;
}
getDescription() {
return `${this.vehicle.name} ${this.model}`;
}
}
class AcSubEngineDecorator extends VehicleDecorator {
getPrice() {
return this.vehicle.getPrice() + 1;
}
getDescription() {
return `${this.vehicle.getDescription()} with AC Sub Engine`;
}
}
// Usage
let bus = new Vehicle('Volvo');
bus = new ModelDecorator(bus, 'B7R');
bus = new AcSubEngineDecorator(bus);
console.log(bus.getDescription()); // "Volvo B7R with AC Sub Engine"
console.log(bus.getPrice()); // 3
Facade Design Patterns
A facade is a front or wrapper that hides complex logic behind it.
In our system, after checkout, consider using Stripe to make the payment.
// initiate stripe
const stripe = {
pay: (amount) => {}
};
const purchase = (itemName, price) => {
stripe.pay(price);
}
Later Stripe may become expensive compared to PayPal, and they want to update the payment system from Stripe to PayPal. To do this, we need to update the purchase method.
// initiate PayPal
const paypal = {
pay: (amount) => {}
};
const purchase = (itemName, price) => {
paypal.pay(price);
}
Instead, if we isolate the functionalities of payment in a separate module, this code refactoring will be much easier,
// Complex subsystem 1
const stripe = {
pay: (amount) => {
// Complex Stripe payment logic
}
};
// Complex subsystem 2
const paypal = {
pay: (amount) => {
// Complex PayPal payment logic
}
};
// Facade
const paymentFacade = {
makePayment: (amount) => {
// Can switch between stripe/paypal easily
return stripe.pay(amount);
// or return paypal.pay(amount);
}
};
// Client usage
const purchase = (itemName, price) => {
// Client only needs to know about the facade
paymentFacade.makePayment(price);
}
In this case, there is a wrapper over the stripe/PayPal module and it will help keep the existing codes without modification.
Another example can be, data fetching functionality. Let’s say we use axios for fetching data. Later we may decide to use the native fetch module. If all the data fetching functionalities are in a separate module, it would be easy to refactor the code base later while switching between fetch and axios module.
Adapter Pattern
When two functionalities are incompatible, an adapter pattern comes between them and makes them compatible. A real-world scenario can be, we used 3.5 mm audio jack and later typeC input came. To use the typeC input with a 3.5 mm audio jack, we do use a converter or adapter.
Consider an example, we have an old calculator where we invoke the add and subtract method separately. However, we invoke the compute operation using operation names and values in the new calculator. Here the adapter class will integrate these two classes.
Consider following calculator class, it simply does addition and subtraction,
// Target Interface (what client expects)
class ModernCalculator {
compute(operation, x, y) {
throw new Error('Method not implemented');
}
}
// Legacy Calculator (Adaptee)
class CalculatorCore {
constructor(num1, num2) {
this.num1 = num1;
this.num2 = num2;
}
add() {
return this.num1 + this.num2;
}
subtract() {
return this.num1 - this.num2;
}
}
// Adapter
class CalculatorAdapter extends ModernCalculator {
compute(operation, x, y) {
const calculator = new CalculatorCore(x, y);
switch(operation.toUpperCase()) {
case 'ADD':
return calculator.add();
case 'SUBTRACT':
return calculator.subtract();
default:
throw new Error(`Unsupported operation: ${operation}`);
}
}
}
// Client code
function performCalculation(calculator, operation, x, y) {
try {
return calculator.compute(operation, x, y);
} catch (error) {
console.error(error.message);
}
}
// Usage
const modernCalc = new CalculatorAdapter();
console.log(performCalculation(modernCalc, 'ADD', 10, 6)); // 16
console.log(performCalculation(modernCalc, 'SUBTRACT', 10, 6)); // 4
Proxy Design Pattern
The proxy design pattern creates the same class as the original implementation and reduces operation. Consider the following CurrencyAPI class. The method getCurrency takes a coinName and fetch a long-running API call to get the currency.
If we look for the currency of a bitCoin it will fetch the API every single time. We can reduce the operation by making use of a Proxy Class.
class CurrencyAPI {
// long running api call
getCurrency(coinName) {
switch(coinName) {
case 'BitCoin':
return '100$';
case 'Ethereium':
return '80$';
default:
return 'Invalid Coin';
}
}
}
const fetch = new CurrencyAPI();
// getting from long api call
console.log(fetch.getCurrency('BitCoin')); // 100$
// getting from long api call
console.log(fetch.getCurrency('BitCoin')); // 100$
According to the Proxy Design Pattern, we created a similar class called ProxyCurrencyAPI. It uses the original CurrencyAPI and additionally caches the result. For the first time, it fetches the API and caches it, and from the next call, it retrieves the results from the cache.
class CurrencyAPI {
getCurrency(coinName) {
switch(coinName) {
case 'BitCoin':
return '100$';
case 'Ethereium':
return '80$';
default:
return 'Invalid Coin';
}
}
}
class ProxyCurrencyAPI {
cache = {};
constructor() {
this.fetch = new CurrencyAPI();
}
getCurrency(coinName) {
if (this.cache[coinName]) {
return this.cache[coinName];
}
this.cache[coinName] = this.fetch.getCurrency(coinName);
return this.cache[coinName];
}
}
const fetch = new ProxyCurrencyAPI();
// fetch from api
console.log(fetch.getCurrency('BitCoin')); // 100$
// retrieve from cache
console.log(fetch.getCurrency('BitCoin')); // 100$
Chain of Responsibility Pattern
The Chain of Responsibility pattern is a behavioral design pattern that passes requests along a chain of handlers. Upon receiving a request, each handler decides whether to:
Process the request or
Pass it to the next handler in the chain
Here's a real-world analogy: Think of a corporate hierarchy where an employee submits an expense report. Based on the amount:
A supervisor can approve expenses under $1000
A manager can approve up to $5000
A director handles anything above $5000
Let me demonstrate with a practical example:
class ExpenseHandler {
constructor() {
this.successor = null;
this.approvalLimit = 0;
}
setSuccessor(successor) {
this.successor = successor;
}
handleExpense(amount) {
if (amount <= this.approvalLimit) {
this.approve(amount);
} else if (this.successor) {
this.successor.handleExpense(amount);
} else {
console.log(`Amount ${amount} needs higher approval`);
}
}
approve(amount) {
console.log(`${this.constructor.name} approved expense of $${amount}`);
}
}
class Supervisor extends ExpenseHandler {
constructor() {
super();
this.approvalLimit = 1000;
}
}
class Manager extends ExpenseHandler {
constructor() {
super();
this.approvalLimit = 5000;
}
}
class Director extends ExpenseHandler {
constructor() {
super();
this.approvalLimit = 10000;
}
}
// Setup the chain
const supervisor = new Supervisor();
const manager = new Manager();
const director = new Director();
supervisor.setSuccessor(manager);
manager.setSuccessor(director);
// Test different expense amounts
supervisor.handleExpense(800); // Handled by Supervisor
supervisor.handleExpense(4500); // Handled by Manager
supervisor.handleExpense(8000); // Handled by Director
supervisor.handleExpense(12000); // Needs higher approval
Command Pattern
Command pattern allows undoing an operation. This can be very useful in some scenarios, like inserting a user in DB and later deciding to undo the operation.
In the following example, the calculator takes a command (EX. AddCommand). It can perform operations and later be able to undo the operation.
class Calculator {
constructor() {
this.value = 0;
this.history = [];
}
executeCommand(command) {
this.value = command.execute(this.value);
this.history.push(command);
}
undo() {
const command = this.history.pop();
this.value = command.undo(this.value);
}
}
class AddCommand {
constructor(valueToBeAdded) {
this.valueToBeAdded = valueToBeAdded;
}
execute(currentValue) {
return currentValue + this.valueToBeAdded;
}
undo(currentValue) {
return currentValue - this.valueToBeAdded;
}
}
const calculator = new Calculator();
calculator.executeCommand(new AddCommand(5));
console.log(calculator.value); // 5
calculator.executeCommand(new AddCommand(7));
console.log(calculator.value); // 12
calculator.undo(); // 5
console.log(calculator.value);
Observer Pattern
In the observer pattern, there is a subject and a couple of observers. Observers are methods, acting as subscribers to the subjects. These observers subscribe to the subject and run according to the changes in the subject.
In the following example, we have a subject, where we can add or remove subscribers.
We can run all the observers using runObservers method after subscribing them. Later it is possible to run this observer in a certain value changes in the Subject.
class Subject {
constructor() {
this.observer = [];
}
subscribe(fn) {
this.observer.push(fn);
}
unsubscribe(fn) {
this.observer = this.observer.filter(f => f !== fn);
}
runObservers() {
this.observer.map(fn => fn());
}
}
const subject = new Subject();
const observer1 = () => console.log('I am observer1 method.');
const observer2 = () => console.log('I am observer2 method.');
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.runObservers();




