Essential JavaScript Design Patterns
Understanding Common Design Patterns with JavaScript

There are numerous programming design patterns that you’ve likely used, but you’re not aware of them.
In 2019, I presented a talk at JS Conf Armenia on this topic. The presentation is in Armenian, and you are welcome to watch it if you’re interested. Today, however, I have chosen to conduct the same research in written form
I will cover some common and useful design patterns, providing JavaScript examples.
In programming, design patterns are invaluable as they provide significant support. By following certain principles, we ultimately achieve well-structured, easily understandable, reusable, and manageable code. When starting the journey of learning design patterns, it may pose challenges, and certain concepts may appear unclear initially, especially when you can express the same idea more briefly. But remember, high-quality code doesn’t always mean it’s the shortest.
As I mentioned earlier, most developers use different kinds of design patterns daily, but they are not aware of them. There are so many design patterns, and each has its principles, scopes, and names.
Design patterns are not programming language-specific. They are more likely programming paradigm-specific. Functional programming, Object-oriented programming, and all those different types of programming paradigms have their design patterns.
Design Pattern types
Think of design patterns as helpful guides in the world of Objects and Classes. When we talk about classes, we are talking also about function constructors. These patterns help us to maintain and organize high-quality code.

According to this map, there are three main types of design patterns.
- Creational Design Patterns focus on the process of object/class creation.
- Structural Design Patterns are concerned with the composition of classes or objects.
- Behavioral Design Patterns focus on the interaction and communication between objects and classes.
We will not delve deeper into each of those design patterns from the image. I just want to introduce some design patterns and their examples. Afterwards, you can dedicate time to books on similar topics. This is something that should be taken seriously, and you can’t learn everything in just a few hours. It requires constant practice.
1. Singleton
This pattern ensures that a class or object has only one instance and provides a global point of access to that instance.
Database connection example
In this example, the MongoClient
is typically used to connect to a MongoDB database. The Singleton pattern here would ensure that there is only one instance of the MongoClient
throughout your application, preventing multiple unnecessary connections to the database.
const { MongoClient } = require('mongodb');
const url = 'mongodb://localhost:27017';
// here you may have other options as well ....
export default MongoClient.connect(url);
When we import and use the exported object in different parts of our application, it will always refer to the same instance of the MongoClient. This helps in managing and efficiently reusing the database connection.
2. Factory
In simple terms, this pattern encapsulates the instantiation logic, providing a way to create objects without specifying their exact class.
class User {
constructor(name) {
this.name = name;
}
}
function createUser(name) {
return new User(name);
}
const myUser = createUser('Sara');
Image example
function createImage(name) {
if (name.match(/\.jpeg$/)) {
return new JpegImage(name);
} if (name.match(/\.gif$/)) {
return new GifImage(name);
} if (name.match(/\.png$/)) {
return new PngImage(name);
}
throw new Exception('Unsupported format!');
}
In this example:
createImage
is the factory method responsible for creating instances of different image types.JpegImage
,GifImage
,PngImage
are the product classes, each representing a specific type of image.
Here are the benefits of using the Factory Pattern in this example:
- Encapsulation: The creation logic is encapsulated in the
createImage
function, isolating it from the rest of the code. - Flexibility: If you want to add a new image format or modify the creation process, you can do it within the factory function without affecting the client code.
- Readability: The client code that uses
createImage
is abstracted from the details of how each image type is created, making the code more readable and maintainable.
Overall, this example nicely demonstrates the Factory Pattern by providing a centralized way to create instances of different image types based on a common interface.
3. Strategy
This is a behavioural design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It allows the client to choose an algorithm from a family of algorithms at runtime.
const ups = new UPS();
const usps = new USPS();
const fedex = new Fedex();
const shipping = new Shipping();
shipping.setStrategy(fedex);
shipping.calculate();
// YOU CAN ALSO DO THIS
// shipping.setStrategy(ups);
// shipping.setStrategy(usps);
In this example:
UPS
,USPS
, andFedex
are different strategies or algorithms for shipping.Shipping
is the context class that uses a strategy.
The key benefit of this Pattern is that it allows the client (Shipping
class in this case) to switch between different algorithms (strategies) at runtime without altering its structure. It promotes code reusability, flexibility, and easy maintenance.
4. Adapter
It allows us to access the functionality of a class using a different interface.
// Existing class with a different interface
class OldSystem {
requestInfo() {
return "Information from the old system.";
}
}
// Adapter to make the OldSystem compatible with the new interface
class Adapter {
constructor(oldSystem) {
this.oldSystem = oldSystem;
}
fetchDetails() {
const oldInfo = this.oldSystem.requestInfo();
return `Adapted: ${oldInfo}`;
}
}
// Using the Adapter we make OldSystem compatible with another interface
const oldSystem = new OldSystem();
const adaptedSystem = new Adapter(oldSystem);
So, the overall purpose is to make an existing class (OldSystem
) work with a new interface by creating an adapter (Adapter
) that translates the methods from the old class to match the expected interface. The client code can then interact with the adapted system (adaptedSystem
) without worrying about the differences in the original and desired interfaces.
5. Command
Here we have a simple calculator with undo functionality using the Command Pattern. It defines basic math operations, creates command objects for each operation, and utilizes a calculator object to execute, undo, and log these operations. The example performs calculations, undoes two operations, and displays the final result. The Command Pattern enables flexible and extensible command execution in a clean and organized manner.
const calculator = new Calculator();
// Perform calculations via commands
calculator.execute(new AddCommand(100));
calculator.execute(new SubCommand(24));
calculator.execute(new MulCommand(6));
calculator.execute(new DivCommand(2));
// Reverse last two commands (undo calculations)
calculator.undo();
calculator.undo();
console.log(`Value: ${calculator.getCurrentValue()}`);
It’s obvious, that this isn’t the complete example. You can find it here.
Why bother writing a basic calculator in 100 lines when it could be done in less? The thing is, the impact may not be obvious immediately.
In big and complicated projects, being clear is super important. Opting for consistent design patterns ensures that you and your team uniformly approach the code. This not only makes the project more understandable but also facilitates smoother collaboration, even when dealing with sizable projects/codebases.
Imagine you’re in an interview, tasked with writing a calculator. How do you think the interviewer would perceive a basic calculator compared to one crafted using this design pattern? It becomes a showcase of your understanding and knowledge, leaving a lasting impression.
6. Module
With the help of this design pattern, we can prevent pollution of the global namespace by keeping variables and functions within a private scope.
Numerous implementations exist, but in this article, I’ll present two straightforward examples that are exceptionally simple and clear within the context of Node.js.
Implementation 1
// utils.js
const privateList = [];
const privateObject = {};
function find() {
// ...
}
function each() {
// ...
}
module.exports = {
find, each
};
Implementation 2
// utils.js
const privateList = [];
const privateObject = {};
exports.find = function () {
// ...
};
exports.each = function () {
// ...
};
In both examples, the functions and variables inside the module are encapsulated (hidden), providing a clean and organized way to structure code. Users of the module can only access what is explicitly exported, helping to maintain a clear separation of concerns and avoiding naming conflicts with other parts of the codebase.
7. Pub/Sub
This behavioral design pattern facilitates communication between different parts of a software system without them having to directly reference each other. It involves a mechanism where entities can subscribe to receive messages (publishers) and other entities can publish messages to the subscribers.
const subscriber1 = new Subscriber();
const subscriber2 = new Subscriber();
const publisher = new Publisher();
subscriber1.sub('t.me/javascript', (msg) => {
console.log(msg);
});
subscriber2.sub('t.me/javascript', (msg) => {
console.log(msg);
});
publisher.pub('t.me/javascript', 'Quiz #123');
Subscriber
is a class that represents an entity that wants to receive messages. Each instance of Subscriber
can subscribe to specific channels. And the Publisher
is a class that represents an entity that sends out messages to specific channels.
Every time specified callback function will be executed when a message is published to that channel.
In summary, the Pub/Sub pattern provides a flexible way for different components or modules in a system to communicate without being directly aware of each other. Publishers send messages to specific channels, and subscribers express interest in receiving messages from particular channels, creating a decoupled and scalable communication system.
8. Observer
In the world of design patterns, numerous patterns may seem similar at first glance, but in reality, they have subtle differences. Similarly, this design pattern shares similarities with the Pub/Sub pattern.
const observer = new Observable();
// Subscriber 1
observer.subscribe('channelName', (msg) => console.log(msg)); // Hello
// Subscriber 2
observer.subscribe('channelName2', (msg) => console.log(msg)); // Hello
observer.notify('channel', 'Hello');
You can find the full example here.
Number of Subjects
In the Observer pattern, there’s typically one subject (or observable) that maintains a list of dependents (observers) and notifies them of changes in its state. But in Pub/Sub, there can be multiple publishers and multiple subscribers, and they communicate through channels. Publishers send messages to specific channels, and subscribers express interest in receiving messages from particular channels.
Flexibility and Scalability
Pub/Sub is more flexible and scalable, especially in distributed systems. Publishers and subscribers can be added or removed without directly impacting each other, making it easier to extend the system. However the Observer pattern might require more careful management of dependencies, and adding or removing observers may involve modifications to the subject.
The granularity of Notification
In the Observer pattern, when the subject notifies observers, it sends the same message to all observers. In Pub/Sub, when a message is published to a channel, only the subscribers of that channel receive the message.
Coupling
The Observer pattern often involves a more direct relationship between the subject and its observers. Observers are aware of the subject and its changes.
Pub/Sub promotes a more decoupled approach. Publishers and subscribers are not explicitly aware of each other; they communicate through a central hub (the message broker) without direct references.
While the design patterns may appear similar, there are crucial differences between them.
9. Constructor
This is a way to define and initialize object instances with their properties and methods.
class Movie {
constructor(name, year) {
this.name = name;
this.year = year;
this.about = () => {
return `${name} movie has been shot in ${year}`;
};
}
}
const hp = new Movie('Harry Potter', 2001);
const insatiable = new Movie('John Wick', 2014);
The Constructor Pattern provides a convenient way to create multiple instances of objects with shared properties and methods.
The Constructor Pattern is often associated with classes in JavaScript, but it’s not limited to classes. In JavaScript, a constructor is essentially a function that creates instances of objects.
function Movie(name, year) {
this.name = name;
this.year = year;
this.about = () => {
return `${name} movie has been shot in ${year}`;
};
}
const johnWick = new Movie('John Wick', 2014);
It’s more about the concept of using a function (whether a class constructor or a regular function) to create and initialize objects. It’s a way to structure and create instances of objects with shared properties and methods.
function createMovie(name, year) {
const about = () => {
return `${name} movie has been shot in ${year}`;
};
return {
name, year, about
};
}
Summary
Remember, every design pattern can have multiple implementations, and implementations can differ in every language. However, the idea is the same.
Every design pattern comes with its pros and cons.
The cool thing is, that these patterns work in all sorts of programming languages. So, if you learn them in Java, you can apply the same tricks in JavaScript.
It’s like having both advantages and disadvantages, but in a language that all programmers can understand!
Consider using up-to-date books to understand those patterns in your favorite programming language. For JavaScript, here are a few examples of helpful books.
- Node.js Design Patterns by Mario Casciaro
- Learning JavaScript Design Patterns by Addy Osmani
Thank you for taking the time to read this article. I hope you found it valuable. Feel free to ask any questions or tweet me @nairihar
Also follow my JavaScript newsletter on Telegram: @javascript