DEV Community

Sai Swaroop Bijinapalli
Sai Swaroop Bijinapalli

Posted on

JavaScript Design Patterns: Singleton and Factory Explained with Real Examples

JavaScript Design Patterns: Singleton and Factory Explained with Real Examples

When developing software, we often face the same types of problems repeatedly.

For example:

  • How should we create objects?
  • Should every part of the application create its own object?
  • How can multiple parts of an application share the same object?
  • How can we add new types of objects without changing existing code?

Instead of solving these problems from scratch every time, software engineering provides design patterns.

This blog focuses on understanding design patterns conceptually and then implementing two important patterns in JavaScript:

  • Singleton Pattern
  • Factory Pattern

What Is a Design Pattern?

A design pattern is a reusable solution or approach to a commonly occurring software design problem.

It is important to understand that a design pattern is not a library or a ready-made piece of code.

It is more like a blueprint.

For example, an architect may have a standard approach for designing a particular type of building. The actual building can be different, but the underlying design idea can be reused.

Similarly, a design pattern gives us a proven way to structure software.

Why Do We Need Design Patterns?

Imagine an application where every developer solves object creation differently.

One developer writes:

```js id="cxk6wq"
const user = {
name: "Sai"
};




Another creates a class:



```js id="f3yykp"
class User {
    constructor(name) {
        this.name = name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Another creates a factory function:

```js id="i6gtdj"
function createUser(name) {
return {
name
};
}




All of these can work, but as applications become larger, we need consistent and maintainable approaches.

Design patterns provide commonly understood structures.

---

# Categories of Design Patterns

Design patterns are commonly grouped into categories.

### Creational Patterns

These deal with **object creation**.

Examples:

* Singleton
* Factory
* Builder

### Structural Patterns

These deal with how objects and classes are combined.

Examples:

* Adapter
* Decorator
* Facade

### Behavioral Patterns

These deal with communication and behavior between objects.

Examples:

* Observer
* Strategy
* Command

For this part of Week-06, the main focus is on **creational patterns**, particularly Singleton and Factory.

---

# Singleton Pattern

Let's start with the Singleton Pattern.

The easiest way to understand Singleton is:

> **One object should exist, and everyone who needs it should use that same object.**

This is the key idea.

---

# Why Would We Need Only One Object?

Consider application configuration.

Suppose our application has:



```text id="d8pjk6"
Application Name
Environment
Port
API URL
Database Configuration
Enter fullscreen mode Exit fullscreen mode

You could create a configuration object in every module:

```text id="gqaf3a"
user.js → Configuration A
payment.js → Configuration B
server.js → Configuration C
database.js → Configuration D




Now we have multiple configuration objects.

That can cause inconsistency.

Instead, we may want:



```text id="6ctf3b"
                  Configuration
                       ↑
          ┌────────────┼────────────┐
          │            │            │
       user.js     payment.js    server.js
Enter fullscreen mode Exit fullscreen mode

Every module accesses the same configuration instance.

This is where Singleton becomes useful.


Singleton in Simple Terms

Think about a Singleton like a single shared resource.

The rule is:

```text id="z4j0wb"
First request

Create object

Second request

Return existing object

Third request

Return existing object




The object is created only once.

---

# Singleton Example

Let's create a configuration manager.



```js id="jv4o4s"
class ConfigManager {

    constructor() {

        if (ConfigManager.instance) {
            return ConfigManager.instance;
        }

        this.config = {
            appName: "My Application",
            environment: "development",
            port: 3000
        };

        ConfigManager.instance = this;
    }

    get(key) {
        return this.config[key];
    }

    set(key, value) {
        this.config[key] = value;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now:

```js id="l8o5m0"
const config1 = new ConfigManager();
const config2 = new ConfigManager();

console.log(config1 === config2);




Output:



```text id="5l5xwi"
true
Enter fullscreen mode Exit fullscreen mode

Why?

Because the second call doesn't create a new object.

It returns the already existing instance.


Understanding the Singleton Step by Step

Initially:

```text id="90s1k3"
ConfigManager.instance

undefined




First:



```js id="8k0xkb"
const config1 = new ConfigManager();
Enter fullscreen mode Exit fullscreen mode

There is no existing instance, so the constructor creates one.

```text id="7xkwcg"
ConfigManager.instance

Object A




Now:



```js id="a0a52k"
const config2 = new ConfigManager();
Enter fullscreen mode Exit fullscreen mode

The constructor checks:

```js id="5z0zsk"
if (ConfigManager.instance) {
return ConfigManager.instance;
}




An instance already exists.

Therefore:



```text id="o2i90s"
config1 ─────┐
             ↓
          Object A
             ↑
config2 ─────┘
Enter fullscreen mode Exit fullscreen mode

Both variables reference the same object.


Singleton and Shared State

This is another important concept.

Suppose:

```js id="vym9qz"
config1.set("port", 5000);




Now:



```js id="8a3rwh"
console.log(config2.get("port"));
Enter fullscreen mode Exit fullscreen mode

Output:

```text id="3vpt8x"
5000




Why?

Because `config1` and `config2` are not two independent objects.

They reference the same object.

This means changes made through one reference are visible through the other.

---

# When Can Singleton Be Useful?

Common examples include:

### Configuration Manager

One application configuration.

### Logger

A centralized logging system.

### Database Connection Manager

A shared connection-management object.

### Cache Manager

One shared cache.

However, Singleton should not automatically be used everywhere.

Creating global shared state can also make applications harder to test and reason about.

The important principle is:

> Use Singleton when having exactly one shared instance is actually part of the application's requirements.

---

# Factory Pattern

Now let's look at the Factory Pattern.

The Factory Pattern solves a different problem.

Singleton asks:

> **"How can I make sure there is only one object?"**

Factory asks:

> **"How can I centralize the creation of different objects?"**

---

# Factory in Simple Terms

Imagine a factory in the real world.

You tell the factory:



```text id="78shm2"
"Build me a car"
Enter fullscreen mode Exit fullscreen mode

The factory handles the details of producing the car.

You don't need to know every internal step.

Similarly, a JavaScript Factory can receive information and create the appropriate object.


Simple Factory Function

Consider:

```js id="n9b4ex"
function createEmployee(name, role) {

return {
    name,
    role
};
Enter fullscreen mode Exit fullscreen mode

}




Now:



```js id="s3pqg0"
const employee1 = createEmployee("Sai", "Developer");
const employee2 = createEmployee("Rahul", "Tester");
Enter fullscreen mode Exit fullscreen mode

The factory function creates objects for us.

The important idea is:

```text id="2x0uyg"
Input

Factory

Object




---

# Why Use a Factory?

Imagine an application that supports different notification types:



```text id="g5th0w"
Email
SMS
Push
Enter fullscreen mode Exit fullscreen mode

Without a Factory, different parts of the application might directly create these objects:

```js id="d5xv7y"
new EmailNotification();
new SMSNotification();
new PushNotification();




Now object creation logic is spread throughout the application.

Instead, we can centralize it:



```js id="m4e6km"
NotificationFactory.create("email");
NotificationFactory.create("sms");
NotificationFactory.create("push");
Enter fullscreen mode Exit fullscreen mode

The Factory decides which object needs to be created.


Notification Factory Example

```js id="8h6wkw"
class EmailNotification {

send(message) {
    console.log(`Email: ${message}`);
}
Enter fullscreen mode Exit fullscreen mode

}

class SMSNotification {

send(message) {
    console.log(`SMS: ${message}`);
}
Enter fullscreen mode Exit fullscreen mode

}

class PushNotification {

send(message) {
    console.log(`Push: ${message}`);
}
Enter fullscreen mode Exit fullscreen mode

}




Now create the Factory:



```js id="7s2k3p"
class NotificationFactory {

    static create(type) {

        switch (type) {

            case "email":
                return new EmailNotification();

            case "sms":
                return new SMSNotification();

            case "push":
                return new PushNotification();

            default:
                throw new Error("Unsupported notification type");
        }

    }

}
Enter fullscreen mode Exit fullscreen mode

Now the application can simply say:

```js id="x1k9jf"
const notification =
NotificationFactory.create("email");

notification.send("Welcome!");




Output:



```text id="e47g4k"
Email: Welcome!
Enter fullscreen mode Exit fullscreen mode

The application doesn't need to know the details of how the EmailNotification object was created.


Understanding the Factory Flow

For Email:

```text id="7unxob"
NotificationFactory
|
| "email"

EmailNotification
|

send()




For SMS:



```text id="iz0lqf"
NotificationFactory
        |
        | "sms"
        ↓
SMSNotification
        |
        ↓
send()
Enter fullscreen mode Exit fullscreen mode

For Push:

```text id="4dd20p"
NotificationFactory
|
| "push"

PushNotification
|

send()




The Factory hides the object creation details.

---

# Factory vs Singleton

This is one of the most important differences to understand.

| Singleton                             | Factory                                 |
| ------------------------------------- | --------------------------------------- |
| Controls the number of instances      | Controls object creation                |
| Usually provides one shared instance  | Can create many objects                 |
| Focuses on reuse of the same instance | Focuses on selecting/creating an object |
| Example: Config Manager               | Example: Notification Factory           |

### Singleton



```text id="f5y22a"
getInstance()
     ↓
Same Object
     ↓
Same Object
     ↓
Same Object
Enter fullscreen mode Exit fullscreen mode

Factory

```text id="p5j1vh"
create("email")

Email Object

create("sms")

SMS Object

create("push")

Push Object




This difference is extremely important.

---

# Connecting Factory with Open/Closed Thinking

Now we can connect this topic to the **Open/Closed Principle**.

The Open/Closed Principle states:

> **Software should be open for extension but closed for modification.**

Suppose our application initially supports:



```text id="m2p5p4"
Email
SMS
Push
Enter fullscreen mode Exit fullscreen mode

Later, we want to add:

```text id="d5d0fj"
WhatsApp




The Factory-based design gives us a clear place to extend the system.

We can introduce:



```js id="b0fkli"
class WhatsAppNotification {

    send(message) {
        console.log(`WhatsApp: ${message}`);
    }

}
Enter fullscreen mode Exit fullscreen mode

Then extend the creation logic to support it.

The larger architectural idea is:

```text id="v7j6tq"
Existing functionality

stays stable

New functionality

added




This is the kind of thinking the Open/Closed Principle encourages.

---

# Important Difference: Pattern vs Principle

These concepts are related but not the same.

### Design Pattern

A reusable design approach.

Example:



```text id="0exzfk"
Singleton
Factory
Observer
Enter fullscreen mode Exit fullscreen mode

Design Principle

A guideline for designing maintainable software.

Example:

```text id="c0zq1p"
Open/Closed Principle




So:



```text id="r5iyj8"
Design Pattern
     ↓
Reusable solution

Design Principle
     ↓
Guideline for better design
Enter fullscreen mode Exit fullscreen mode

Real-World Example

Imagine an e-commerce application.

Singleton

The application has one configuration manager:

```text id="bqu3hw"
Application

Config Manager

One Instance




### Factory

The application supports different notifications:



```text id="4xevq5"
Notification Factory
      |
      ├── Email
      ├── SMS
      └── Push
Enter fullscreen mode Exit fullscreen mode

Observer

When an order is placed:

```text id="8u0q67"
Order Placed

Event

| | |
Email SMS Analytics




Each pattern solves a different problem.

---

# The Three Patterns at a Glance

### Singleton

**Problem:**

> I need exactly one shared instance.

**Solution:**

> Create it once and return the same instance.

---

### Factory

**Problem:**

> I have different object types and don't want object creation logic scattered throughout my application.

**Solution:**

> Centralize object creation in a Factory.

---

### Observer

**Problem:**

> Multiple parts of the application need to react when something happens.

**Solution:**

> Allow listeners/observers to subscribe and notify them when an event occurs.

---

# Final Mental Model

The easiest way to remember these patterns is:



```text id="f9e2ul"
                 DESIGN PATTERNS
                       |
        --------------------------------
        |              |               |
    Singleton        Factory        Observer
        |              |               |
   ONE object      CREATE objects    NOTIFY objects
        |              |               |
   Config         Email/SMS/Push      Events
Enter fullscreen mode Exit fullscreen mode

The patterns are not just code tricks. They are ways of thinking about software design.

Singleton helps control instance creation.

Factory helps control object creation.

Observer helps control communication between objects.

Together with principles such as the Open/Closed Principle, these ideas help us build JavaScript applications that are easier to understand, extend, test, and maintain.

Top comments (0)