DEV Community

Sai Swaroop Bijinapalli
Sai Swaroop Bijinapalli

Posted on

JavaScript Week 06: Modules, Design Patterns, and Memory Management

A practical guide to CommonJS, ESM, design patterns, closures, garbage collection, memory leaks, and the Open/Closed Principle.

Introduction

As JavaScript applications grow, writing code that simply works is not enough. We also need code that is organized, reusable, maintainable, and easy to extend.

In this week, I explored several concepts that help achieve this:

  • CommonJS and ESM modules
  • Design patterns
  • Singleton Pattern
  • Factory Pattern
  • Observer Pattern
  • Pub/Sub Pattern
  • Module Pattern
  • JavaScript memory management
  • Closures
  • Garbage Collection
  • Memory Leaks
  • Open/Closed Principle

These concepts are connected. Modules help organize code, design patterns provide reusable solutions to common problems, and memory management helps us understand how JavaScript handles the data created by our applications.


1. What Are JavaScript Modules?

A module is a separate file containing code that can be reused by other files.

Instead of putting the entire application into one large file:

app.js
├── user logic
├── database logic
├── authentication
├── notification logic
└── utility functions
Enter fullscreen mode Exit fullscreen mode

we can divide it into smaller modules:

project/
├── user.js
├── database.js
├── auth.js
├── notification.js
└── app.js
Enter fullscreen mode Exit fullscreen mode

This makes the application easier to understand and maintain.

JavaScript mainly uses two module systems:

  • CommonJS
  • ES Modules (ESM)

2. CommonJS

CommonJS is a module system that has traditionally been widely used in Node.js applications.

It uses:

require()
Enter fullscreen mode Exit fullscreen mode

for importing and:

module.exports
Enter fullscreen mode Exit fullscreen mode

for exporting.

Example

math.js

function add(a, b) {
    return a + b;
}

function subtract(a, b) {
    return a - b;
}

module.exports = {
    add,
    subtract
};
Enter fullscreen mode Exit fullscreen mode

Now another file can import these functions.

app.js

const { add, subtract } = require("./math");

console.log(add(10, 5));
console.log(subtract(10, 5));
Enter fullscreen mode Exit fullscreen mode

Output:

15
5
Enter fullscreen mode Exit fullscreen mode

The main idea is:

math.js
   ↓
module.exports
   ↓
app.js
   ↓
require()
Enter fullscreen mode Exit fullscreen mode

3. ES Modules (ESM)

ES Modules are the standardized modern module system in JavaScript.

ESM uses:

export
Enter fullscreen mode Exit fullscreen mode

and:

import
Enter fullscreen mode Exit fullscreen mode

Example

math.js

export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
Enter fullscreen mode Exit fullscreen mode

app.js

import { add, subtract } from "./math.js";

console.log(add(10, 5));
console.log(subtract(10, 5));
Enter fullscreen mode Exit fullscreen mode

In Node.js, ESM can be enabled by setting:

{
    "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

in package.json.


4. CommonJS vs ESM

CommonJS ESM
Uses require() Uses import
Uses module.exports Uses export
Traditionally common in Node.js Modern JavaScript standard
Common in older Node.js projects Common in modern applications
Example: require("./math") Example: import { add } from "./math.js"

Simple way to remember

CommonJS → require + module.exports

ESM → import + export
Enter fullscreen mode Exit fullscreen mode

Both solve the same general problem: organizing and sharing code between files.


5. What Are Design Patterns?

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

A design pattern is not a library or framework. It is more like a proven approach to structuring code.

For example, suppose multiple components need to know when a particular event occurs.

Without a pattern, we might create many direct connections:

Component A → Component B
Component A → Component C
Component A → Component D
Enter fullscreen mode Exit fullscreen mode

As the application grows, this becomes difficult to maintain.

A design pattern such as Observer can provide a cleaner structure:

             Event Source
                  |
        ┌─────────┼─────────┐
        ↓         ↓         ↓
     Observer  Observer  Observer
Enter fullscreen mode Exit fullscreen mode

Design patterns help improve:

  • Reusability
  • Maintainability
  • Flexibility
  • Scalability
  • Code organization
  • Communication between developers

6. Singleton Pattern

The Singleton Pattern ensures that only one instance of an object exists and provides a common way to access it.

Imagine an application configuration:

              Application
                   |
             Config Manager
                   |
        ┌──────────┼──────────┐
        ↓          ↓          ↓
       API         DB       Services
Enter fullscreen mode Exit fullscreen mode

We may want all parts of the application to use the same configuration object.

Example

class Config {
    constructor() {
        if (Config.instance) {
            return Config.instance;
        }

        this.port = 5000;
        this.database = "PostgreSQL";

        Config.instance = this;
    }
}

const config1 = new Config();
const config2 = new Config();

console.log(config1 === config2);
Enter fullscreen mode Exit fullscreen mode

Output:

true
Enter fullscreen mode Exit fullscreen mode

Both variables refer to the same instance.

Common use cases

Singleton can be useful for:

  • Application configuration
  • Logging
  • Cache managers
  • Shared service managers

However, Singleton should not be used everywhere. Excessive use can make testing and dependency management more difficult.


7. Factory Pattern

The Factory Pattern is a creational pattern used to create objects without requiring the calling code to know the exact creation logic.

Imagine a notification system supporting:

  • Email
  • SMS
  • Push notifications

Instead of creating each object directly, we can use a factory.

              Notification Factory
                      |
             ┌────────┼────────┐
             ↓        ↓        ↓
           Email      SMS     Push
Enter fullscreen mode Exit fullscreen mode

Example

class EmailNotification {
    send() {
        console.log("Sending Email");
    }
}

class SMSNotification {
    send() {
        console.log("Sending SMS");
    }
}

class PushNotification {
    send() {
        console.log("Sending Push Notification");
    }
}

function createNotification(type) {
    if (type === "email") {
        return new EmailNotification();
    }

    if (type === "sms") {
        return new SMSNotification();
    }

    if (type === "push") {
        return new PushNotification();
    }

    throw new Error("Unknown notification type");
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const notification = createNotification("email");

notification.send();
Enter fullscreen mode Exit fullscreen mode

Output:

Sending Email
Enter fullscreen mode Exit fullscreen mode

The caller only needs to specify the type:

createNotification("email");
Enter fullscreen mode Exit fullscreen mode

It does not need to know how EmailNotification is constructed.

Common use cases

Factory patterns are useful for:

  • Notifications
  • Database drivers
  • Payment methods
  • UI components
  • Different service implementations

8. Observer Pattern

The Observer Pattern allows one object to notify multiple objects when something changes.

The object being observed is commonly called the Subject, while the objects receiving notifications are called Observers.

                 Subject
                    |
                 notify()
                    |
        ┌───────────┼───────────┐
        ↓           ↓           ↓
    Observer A  Observer B  Observer C
Enter fullscreen mode Exit fullscreen mode

Example

class Subject {
    constructor() {
        this.observers = [];
    }

    subscribe(observer) {
        this.observers.push(observer);
    }

    notify(message) {
        this.observers.forEach(observer => {
            observer(message);
        });
    }
}

const subject = new Subject();

subject.subscribe(message => {
    console.log("User A:", message);
});

subject.subscribe(message => {
    console.log("User B:", message);
});

subject.notify("New video uploaded!");
Enter fullscreen mode Exit fullscreen mode

Output:

User A: New video uploaded!
User B: New video uploaded!
Enter fullscreen mode Exit fullscreen mode

A real-world example is a YouTube channel.

When a channel uploads a new video, multiple subscribers can be notified.


9. Pub/Sub Pattern

Pub/Sub stands for Publisher/Subscriber.

It is similar to Observer, but the publisher and subscribers communicate through an intermediate event system, often called an Event Bus.

Publisher
    |
    ↓
 Event Bus
    |
    ├──→ Subscriber A
    ├──→ Subscriber B
    └──→ Subscriber C
Enter fullscreen mode Exit fullscreen mode

The publisher does not need to know who the subscribers are.

Example

class EventBus {
    constructor() {
        this.events = {};
    }

    on(event, callback) {
        if (!this.events[event]) {
            this.events[event] = [];
        }

        this.events[event].push(callback);
    }

    emit(event, data) {
        if (!this.events[event]) {
            return;
        }

        this.events[event].forEach(callback => {
            callback(data);
        });
    }
}

const bus = new EventBus();

bus.on("login", username => {
    console.log(`${username} logged in`);
});

bus.emit("login", "Swaroop");
Enter fullscreen mode Exit fullscreen mode

Output:

Swaroop logged in
Enter fullscreen mode Exit fullscreen mode

Observer vs Pub/Sub

The main difference is coupling.

Observer:

Subject → Observers
Enter fullscreen mode Exit fullscreen mode

The subject generally knows about its observers.

Pub/Sub:

Publisher → Event Bus → Subscribers
Enter fullscreen mode Exit fullscreen mode

The publisher and subscribers are more independent.

Pub/Sub is especially useful in event-driven systems.


10. Module Pattern

The Module Pattern is used to organize code and provide encapsulation.

One important benefit is keeping some data private while exposing only selected functions.

Example

const counter = (function () {

    let count = 0;

    return {
        increment() {
            count++;
        },

        getCount() {
            return count;
        }
    };

})();
Enter fullscreen mode Exit fullscreen mode

Usage:

counter.increment();
counter.increment();

console.log(counter.getCount());
Enter fullscreen mode Exit fullscreen mode

Output:

2
Enter fullscreen mode Exit fullscreen mode

But:

console.log(counter.count);
Enter fullscreen mode Exit fullscreen mode

returns:

undefined
Enter fullscreen mode Exit fullscreen mode

because count is not directly exposed.

The structure is:

Module
│
├── Private Data
│   └── count
│
└── Public API
    ├── increment()
    └── getCount()
Enter fullscreen mode Exit fullscreen mode

The Module Pattern is strongly connected to closures.


11. JavaScript Memory Basics

Whenever JavaScript creates values and objects, memory is required to store them.

A simplified view is:

JavaScript Memory
       |
   ┌───┴────┐
   ↓        ↓
 Stack     Heap
Enter fullscreen mode Exit fullscreen mode

Stack

The stack is commonly associated with:

  • Function calls
  • Execution contexts
  • Primitive values and references

Heap

The heap is commonly used for dynamically allocated objects and other data.

For example:

const user = {
    name: "Swaroop"
};
Enter fullscreen mode Exit fullscreen mode

The object is stored in memory, while the variable holds a reference to it.

The exact memory implementation is engine-specific, but this stack/heap model is useful for understanding JavaScript memory behavior.


12. Garbage Collection

JavaScript automatically manages memory using Garbage Collection (GC).

The basic idea is that objects that are no longer reachable can eventually have their memory reclaimed.

For example:

let user = {
    name: "Swaroop"
};

user = null;
Enter fullscreen mode Exit fullscreen mode

After user is changed to null, the original object may no longer be reachable.

Conceptually:

Object
  ↑
 user
Enter fullscreen mode Exit fullscreen mode

After:

user = null;
Enter fullscreen mode Exit fullscreen mode

the reference is removed:

Object

(no reachable reference)
Enter fullscreen mode Exit fullscreen mode

The garbage collector can eventually reclaim that memory.

Important point

Garbage collection does not necessarily happen immediately when an object becomes unused.

The JavaScript engine decides when garbage collection should occur.


13. Closures

A closure occurs when a function remembers and can access variables from its surrounding lexical scope, even after the outer function has finished executing.

Example

function createCounter() {
    let count = 0;

    return function () {
        count++;
        return count;
    };
}

const counter = createCounter();

console.log(counter());
console.log(counter());
Enter fullscreen mode Exit fullscreen mode

Output:

1
2
Enter fullscreen mode Exit fullscreen mode

Normally, we might expect count to disappear when createCounter() finishes.

But the returned function still references count.

Therefore, the JavaScript engine keeps the required environment reachable.

createCounter()
      |
      ↓
 count = 0
      |
      ↓
returned function
      |
      ↓
remembers count
Enter fullscreen mode Exit fullscreen mode

Closures are useful for:

  • Private variables
  • Encapsulation
  • Function factories
  • Callbacks
  • Maintaining state

14. Closures and Memory

Closures are not automatically a problem.

In fact, closures are one of the most useful features of JavaScript.

However, a closure can keep references to data alive for as long as the closure itself remains reachable.

For example:

function createFunction() {
    const largeData = new Array(1000000).fill("data");

    return function () {
        console.log(largeData.length);
    };
}

const fn = createFunction();
Enter fullscreen mode Exit fullscreen mode

The returned function references largeData, so that data remains reachable through the closure.

If a long-lived object keeps fn alive unnecessarily, the referenced data may also remain in memory.

This is why understanding closures is important when debugging memory usage.


15. What Is a Memory Leak?

A memory leak happens when an application continues to hold references to memory that it no longer needs.

Because the memory is still reachable, the garbage collector cannot reclaim it.

Conceptually:

Application
    |
    ↓
Unnecessary reference
    |
    ↓
Object remains reachable
    |
    ↓
Garbage Collector cannot remove it
Enter fullscreen mode Exit fullscreen mode

Common Causes

1. Growing collections

const users = [];

function addUser(user) {
    users.push(user);
}
Enter fullscreen mode Exit fullscreen mode

If the array continues growing forever without removing unnecessary entries, memory usage can increase.

2. Event listeners

When an event listener is no longer needed, it should be removed when appropriate.

element.addEventListener("click", handler);
Enter fullscreen mode Exit fullscreen mode

Later:

element.removeEventListener("click", handler);
Enter fullscreen mode Exit fullscreen mode

3. Timers

A timer that continues running unnecessarily can keep references alive.

const intervalId = setInterval(() => {
    console.log("Running...");
}, 1000);
Enter fullscreen mode Exit fullscreen mode

When it is no longer needed:

clearInterval(intervalId);
Enter fullscreen mode Exit fullscreen mode

4. Unnecessary global references

Large objects stored in long-lived global structures can remain reachable for the lifetime of the application.

5. Long-lived closures

A closure can unintentionally retain references to data that is no longer needed.


16. Design Patterns as Reusable Solutions

The most important thing about design patterns is not memorizing their names.

Instead, think:

What problem am I trying to solve?

Then select an appropriate pattern.

Problem Pattern
Need a single shared instance Singleton
Need flexible object creation Factory
Need to notify dependent objects Observer
Need loosely coupled event communication Pub/Sub
Need private state and encapsulation Module
Need organized code across files Modules

For example:

Problem:
Different parts of an application need the same configuration.

Solution:
Singleton.
Enter fullscreen mode Exit fullscreen mode

Another example:

Problem:
The application supports Email, SMS and Push notifications.

Solution:
Factory.
Enter fullscreen mode Exit fullscreen mode

And:

Problem:
Several components need to react when an event occurs.

Solution:
Observer or Pub/Sub.
Enter fullscreen mode Exit fullscreen mode

This problem-solving mindset is more valuable than simply memorizing pattern definitions.


17. Open/Closed Principle

The Open/Closed Principle (OCP) is one of the SOLID principles.

It states:

Software entities should be open for extension but closed for modification.

In simpler words:

We should be able to add new behavior without repeatedly changing existing, working code.


18. Open/Closed Principle: Bad Example

Consider a notification function:

function sendNotification(type, message) {

    if (type === "email") {
        console.log("Sending Email:", message);
    }

    else if (type === "sms") {
        console.log("Sending SMS:", message);
    }

    else if (type === "push") {
        console.log("Sending Push:", message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Now imagine we want to add WhatsApp.

We have to modify the existing function:

else if (type === "whatsapp") {
    console.log("Sending WhatsApp:", message);
}
Enter fullscreen mode Exit fullscreen mode

If we continue adding notification types, the function keeps growing.

This makes the code harder to maintain.


19. Open/Closed Principle: Better Design

We can separate each notification implementation.

class EmailNotification {
    send(message) {
        console.log("Email:", message);
    }
}

class SMSNotification {
    send(message) {
        console.log("SMS:", message);
    }
}

class PushNotification {
    send(message) {
        console.log("Push:", message);
    }
}
Enter fullscreen mode Exit fullscreen mode

Then use a factory to create the required notification:

function createNotification(type) {

    const notifications = {
        email: EmailNotification,
        sms: SMSNotification,
        push: PushNotification
    };

    const Notification = notifications[type];

    if (!Notification) {
        throw new Error("Unknown notification type");
    }

    return new Notification();
}
Enter fullscreen mode Exit fullscreen mode

Usage:

const notification = createNotification("email");

notification.send("Hello!");
Enter fullscreen mode Exit fullscreen mode

Now the system is easier to extend because each notification type has its own implementation.

This demonstrates how design patterns and design principles can work together.


20. How These Concepts Connect

All of these topics are related to writing better software.

                 JavaScript Application
                         |
        ┌────────────────┼────────────────┐
        ↓                ↓                ↓
     Modules        Design Patterns     Memory
        |                |                |
    ┌───┴───┐       ┌────┼────┐      ┌───┼────┐
    ↓       ↓       ↓    ↓    ↓      ↓   ↓    ↓
CommonJS  ESM   Singleton Factory Observer GC Closures
                              |
                           Pub/Sub
                              |
                         Module Pattern
Enter fullscreen mode Exit fullscreen mode

The relationships can be understood like this:

Modules

Help us organize and separate code.

Design Patterns

Help us solve recurring design problems.

Closures

Help us maintain state and create private data.

Garbage Collection

Helps JavaScript automatically reclaim unreachable memory.

Memory Leak Awareness

Helps developers avoid unnecessarily retaining memory.

Open/Closed Principle

Helps us design software that can grow without constantly modifying existing code.


21. Real-World Example

Consider an e-commerce application.

We could use different concepts together:

Modules

Separate the application:

users.js
products.js
payments.js
notifications.js
Enter fullscreen mode Exit fullscreen mode

Factory

Create different payment methods:

Payment Factory
      |
 ┌────┼────┐
 ↓    ↓    ↓
UPI Card Wallet
Enter fullscreen mode Exit fullscreen mode

Singleton

Maintain shared application configuration.

Observer/Pub/Sub

Notify different parts of the application when an order is placed:

Order Created
      |
      ↓
  Event Bus
   /   |   \
  ↓    ↓    ↓
Email Inventory Analytics
Enter fullscreen mode Exit fullscreen mode

Module Pattern

Keep sensitive internal state private.

Closures

Maintain state inside functions.

Garbage Collection

Reclaim memory when objects are no longer reachable.

Open/Closed Principle

Allow new payment methods or notification types to be added without heavily modifying existing logic.

This is where these concepts become useful in real applications.


22. Key Takeaways

After studying these concepts, the main lessons are:

  1. CommonJS and ESM provide ways to organize and share JavaScript code.
  2. Design patterns are reusable approaches to recurring software design problems.
  3. Singleton provides one shared instance.
  4. Factory centralizes object creation.
  5. Observer allows objects to react to changes.
  6. Pub/Sub provides loosely coupled event communication.
  7. Module Pattern provides encapsulation and private state.
  8. Closures allow functions to retain access to their surrounding variables.
  9. Garbage Collection automatically reclaims unreachable memory.
  10. Memory leaks happen when unnecessary objects remain reachable.
  11. Open/Closed Principle encourages extending functionality without unnecessarily modifying existing code.
  12. Good software design is about choosing the right solution for the problem rather than using patterns everywhere.

Conclusion

Week 06 helped me understand that JavaScript development is not only about writing functions and getting the correct output. As applications become larger, code organization, reusability, maintainability, and memory management become increasingly important.

Modules such as CommonJS and ESM help organize code. Design patterns such as Singleton, Factory, Observer, Pub/Sub, and Module provide reusable approaches to common problems. Closures explain how JavaScript can preserve state, while garbage collection and memory-leak concepts help us understand how memory is managed.

Finally, the Open/Closed Principle introduced an important design mindset: build software that can be extended without constantly changing stable existing code.

Understanding these concepts provides a strong foundation for building larger and more maintainable JavaScript and Node.js applications.

Top comments (0)