Top Design Patterns to master in Node/TypeScript

Over the years I have seen that there is one thing that separates a seasoned developer from a freshman: the extent to which they use Design Patterns!

But What are Design Patterns?

Design patterns are tried and tested holy grail of solutions to the most common code design problems which have stood the test of time.

So why should you care about learning design patterns?

  1. As a developer, implementing solutions that use design patterns will save you hours of debugging time by making your code robust & reusable
  2. It also reduces onboarding time to a new codebase as design patterns transcend particular programming language
  3. It makes maintenance of code easier by keeping your codebase in check with SOLID principles
  4. And it’s the most popular interview question, especially for senior roles

Now that I have convinced you about why you should learn design patterns, let’s look at the most popular patterns which you should know in 2022!

Even though new design patterns are being added every day, all these patterns belong to 3 major categories:

  1. Creational
  2. Structural
  3. Behavioral

Let’s take a deep dive into each one:

Creational patterns

Creational design patterns are concerned with improving the process of creating Objects.

But why do we need this pattern?

It not only enforces a structure but also avoids unnecessary manipulation while creating an object.

Let’s look at the most popular pattern within this category.

Singleton pattern

Sometimes we want to restrict the total number of instances that can be created of a particular object. The reason could be either performance implications or resolving concurrency problems.

Irrespective of that Singleton pattern which stands for singular instance provides an elegant way to handle such a situation. Think of singleton pattern as global variables on steroids without unintentional manipulation.

Real-life analogy:

Wifi router is shared by all the family members. In this case, there is a single wifi connection being consumed by multiple members.

Use case:

Think about initializing a database connection.


  • Restricting the creation of a new object
  • Returning the same instance on every request

Solution: Singleton pattern

In this case, a single DBConnection exposes static instance of the class through getInstance(). Now upon every request, we will get the same instance.

class DBConnection { private static instance: DBConnection | null = null; private constructor() {} public static getInstance(): DBConnection { if (!DBConnection.instance) { console.log("Creating new connection"); DBConnection.instance = new DBConnection(); } else { console.log("Returning existing connection"); } return DBConnection.instance; }
} DBConnection.getInstance();

// Output Creating new connection
Returning existing connection

Now that we have Object Creation Pattern out of the way, let us look at all the patterns that can be employed to improve the structure of objects so that they can communicate effectively with other objects.

Structural Patterns

Structural design patterns aim to ensure that the structure defined for our Classes/Objects is efficient and flexible so that it can be easily composed with other objects. A simple example of this would be whenever you are using extends or implements in your Class, you are employing some form of Structural Design Pattern. There are a lot of patterns under this category but we will focus on the 3 most popular ones that you will always find in your codebase:

    1. Adapter pattern
    2. Module pattern
    3. Composite pattern

Adapter Pattern

The adapter pattern allows two completely different objects to understand and communicate with each other.

Real-life analogy:

A language translator that facilitates communication between people speaking different languages.

Use case:

Think about a Data pipeline that receives event information from different producers which then needs to be stored by consumers in a SQL database like MySQL or PostgreSQL.


    • Because event information is received from different sources with each source having its own data format, to consume this data we would need some additional logic which would make the system unnecessarily complex.
    • Furthermore, this also violates the Single responsibility principle which is a pillar for having a successful application.

Solution: Adapter pattern

If we employ the Adapter pattern, a new class will act as a wrapper whose only job is to transform the incoming event data into a format required by our event consumer class. As a result, two incompatible objects are now compatible. Our implementation would look something like this:

// Event Consumer
interface IEvent { commit(eventId: string, eventName: string): void;
} class EventConsumer implements IEvent { commit(id: string, name: string): void { console.log(`Data (${id}, ${name}) saved in DB!`) }

// Event Producer
interface IEventProducer { eventId: string; eventName: string; sendEvent(consumer: IEvent): void;
} class EventProducer implements IEventProducer { public eventId: string; public eventName: string; constructor(eventId: string, eventName: string) { this.eventId=eventId this.eventName=eventName } sendEvent(consumer: IEvent): void { console.log('Sending event from producer to consumer') consumer.commit(this.eventId, this.eventName) }

// Event Adapter
class EventAdapter implements IEvent { commit(eventId: string, eventName: string): void { console.log('Event received by adapter') const consumer = new EventConsumer() consumer.commit(eventId, eventName) }
} const consumer = new EventAdapter()
const producer = new EventProducer('1', 'add')

// Output Sending event from producer to consumer
Event received by adapter
Data (1, add) saved in DB!

Module Pattern

The best way your application can facilitate Composition is by using a Module pattern. In this case, each module represents an independent part of your application that can be assembled into a more complex application. Employing a module pattern can make your codebase not only organized but also extremely reusable. This pattern solidifies not only the Single Responsibility principle but the Separation of Concerns principle as well.

Real-life analogy:

Lego structure where each lego block is a module and different legos can be assembled together to form beautiful structures.

Use case:

Implementing Auth in different applications.


    • Each application needs to worry about implementing Auth
    • Reimplementation also violates the DRY(Do not Repeat Yourself) principle

Solution: Module pattern

In this case, we will create a separate Auth module which will then be imported & consumed by different applications. Our implementation should look something like below:

Auth module:

// @app/auth.ts interface IAuth { authorize(username: string, password: string): void;
} class Auth implements IAuth { authorize(username: string, password: string): void { console.log(`Authorizing ${username}...`) console.log('Login Succeeded!') }
} export default new Auth() 

User module:

// @app/users.ts
import Auth from "./module"; class User { login(username: string, password: string): void { Auth.authorize(username, password) }
} const user1 = new User()
user1.login('arwa', 'temppass')

// Output Authorizing arwa...
Login Succeeded!

Composite Pattern

You can think of the Composite pattern as an extension of the Module pattern. In this case, once you have created an independent module, you can compose it together with other modules to create a complex application.

Real-life analogy:

Your smartphone, that is made of so many different pieces like a display, battery, microphone, memory unit, camera, and so on. Each of these individual components is not only functional on its own but they can also be composed together to create a complex piece that is your smartphone.

Use case:

Extending on the Auth example, we can now add another independent module like Logger into our Users module to log any significant events. As a result Users module only focuses on User related business logic and imports additional modules as and when needed!

Solution: Composite pattern

In this case, we would have 3 different modules: Auth, Logger & Users that handle authentication & authorization, logging, and User related updates respectively.

The final structure should represent a tree as shown below. So far we have seen patterns describing how objects can be created and structured. Now, let’s look into patterns that describe how objects communicate!

Behavioral patterns

Behavioral patterns aim at defining how objects can communicate with each other in a flexible way. Let’s take a deeper look into the most popular pattern under this category:

Observer pattern

If you have ever used a Publisher/Subscriber model or a message queue or even a UI framework then you have already experienced the power of the Observer pattern.

Observer pattern elegantly handles scenarios where you have one-to-many relationships between objects such that the change in one object notifies all the dependent objects.

There are 2 main flavors of this pattern:

    1. Push based

In this case, the change notification is pushed to subscribers.
Example: sockets or pub/sub model

2.  Pull based

In this case, subscribing objects keep polling the publisher from time to time to get updates.
Example: REST APIs

Real-life analogy:

    1. Push based – A News Channel, that pushes new information to its customers as and when something happens
    2. Pull based – You keep checking your postbox to see if your letter has arrived

Use case:

When you follow an account on social media, you expect to receive all the updates as soon as they are published by the owner account.

Push based Observer Pattern is the perfect solution for this type of code design problem.

Example code:

// Owner Account interface interface IOwnerAccount { accountHandle: string; follow(follower: IFollowerAccount): void; unfollow(follower: IFollowerAccount): void; notify(): void
} class OwnerAccount implements IOwnerAccount { constructor(public readonly accountHandle: string) {} private followers: IFollowerAccount[] = [] follow(follower: IFollowerAccount) { this.followers.push(follower) } unfollow(follower: IFollowerAccount) { this.followers = this.followers.filter(current => { return current.accountHandle!== follower.accountHandle }) } notify() { console.log(`${this.accountHandle} just posted something new!`) this.followers.forEach(follower => { follower.update() }) }

// Follower Account interface interface IFollowerAccount { update(): void; accountHandle: string;
} class FollowerAccount implements IFollowerAccount { constructor(public readonly accountHandle: string) {} update(): void { console.log(`${this.accountHandle} feed is updating...`) }
} const followerOne = new FollowerAccount('@code.with.arwa')
const followerTwo = new FollowerAccount('@johndoe') const ownerAccount = new OwnerAccount('@codewithmosh') ownerAccount.follow(followerOne)

// Output @codewithmosh just posted something new!
@code.with.arwa feed is updating...
@codewithmosh just posted something new!
@code.with.arwa feed is updating...
@johndoe feed is updating...
@codewithmosh just posted something new!
@johndoe feed is updating...


One of the best pieces of advice I ever received from my mentor was to ace the design patterns inside out but I know from experience that design patterns can be daunting. So I hope you find this guide a good starting point. If you liked the post, don’t forget to share it with your network. You can follow me on Instagram @code.with.arwa for more updates.


  • GofPatterns
  • Design patterns: Elements of reusable object-oriented software book

Hi, I am Arwa!! I love to explore new things going around in the web world and write about it. I am also a speaker & an Instructor. When I am not exploring tech world you can probably see me reading a non-fiction book or watching some science-fiction series !!

Tags: backend, clean-code, design-patterns, Interview, javascript, object-oriented programming, TypeScript