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
we can divide it into smaller modules:
project/
├── user.js
├── database.js
├── auth.js
├── notification.js
└── app.js
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()
for importing and:
module.exports
for exporting.
Example
math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
Now another file can import these functions.
app.js
const { add, subtract } = require("./math");
console.log(add(10, 5));
console.log(subtract(10, 5));
Output:
15
5
The main idea is:
math.js
↓
module.exports
↓
app.js
↓
require()
3. ES Modules (ESM)
ES Modules are the standardized modern module system in JavaScript.
ESM uses:
export
and:
import
Example
math.js
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
app.js
import { add, subtract } from "./math.js";
console.log(add(10, 5));
console.log(subtract(10, 5));
In Node.js, ESM can be enabled by setting:
{
"type": "module"
}
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
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
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
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
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);
Output:
true
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:
- SMS
- Push notifications
Instead of creating each object directly, we can use a factory.
Notification Factory
|
┌────────┼────────┐
↓ ↓ ↓
Email SMS Push
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");
}
Usage:
const notification = createNotification("email");
notification.send();
Output:
Sending Email
The caller only needs to specify the type:
createNotification("email");
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
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!");
Output:
User A: New video uploaded!
User B: New video uploaded!
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
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");
Output:
Swaroop logged in
Observer vs Pub/Sub
The main difference is coupling.
Observer:
Subject → Observers
The subject generally knows about its observers.
Pub/Sub:
Publisher → Event Bus → Subscribers
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;
}
};
})();
Usage:
counter.increment();
counter.increment();
console.log(counter.getCount());
Output:
2
But:
console.log(counter.count);
returns:
undefined
because count is not directly exposed.
The structure is:
Module
│
├── Private Data
│ └── count
│
└── Public API
├── increment()
└── getCount()
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
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"
};
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;
After user is changed to null, the original object may no longer be reachable.
Conceptually:
Object
↑
user
After:
user = null;
the reference is removed:
Object
(no reachable reference)
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());
Output:
1
2
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
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();
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
Common Causes
1. Growing collections
const users = [];
function addUser(user) {
users.push(user);
}
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);
Later:
element.removeEventListener("click", handler);
3. Timers
A timer that continues running unnecessarily can keep references alive.
const intervalId = setInterval(() => {
console.log("Running...");
}, 1000);
When it is no longer needed:
clearInterval(intervalId);
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.
Another example:
Problem:
The application supports Email, SMS and Push notifications.
Solution:
Factory.
And:
Problem:
Several components need to react when an event occurs.
Solution:
Observer or Pub/Sub.
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);
}
}
Now imagine we want to add WhatsApp.
We have to modify the existing function:
else if (type === "whatsapp") {
console.log("Sending WhatsApp:", message);
}
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);
}
}
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();
}
Usage:
const notification = createNotification("email");
notification.send("Hello!");
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
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
Factory
Create different payment methods:
Payment Factory
|
┌────┼────┐
↓ ↓ ↓
UPI Card Wallet
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
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:
- CommonJS and ESM provide ways to organize and share JavaScript code.
- Design patterns are reusable approaches to recurring software design problems.
- Singleton provides one shared instance.
- Factory centralizes object creation.
- Observer allows objects to react to changes.
- Pub/Sub provides loosely coupled event communication.
- Module Pattern provides encapsulation and private state.
- Closures allow functions to retain access to their surrounding variables.
- Garbage Collection automatically reclaims unreachable memory.
- Memory leaks happen when unnecessary objects remain reachable.
- Open/Closed Principle encourages extending functionality without unnecessarily modifying existing code.
- 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)