DEV Community

HARSHITH GADDAM
HARSHITH GADDAM

Posted on

My JavaScript Learning Journey: CommonJS vs ESM,Design Patterns

JavaScript Architecture, Design Patterns, Memory Mechanics.

Today I learned about core JavaScript architecture, modules, design patterns, memory mechanics.

Understanding how JavaScript handles files, memory, and software design makes it much easier to write clean, scalable, and maintainable applications.

1. CommonJS (CJS) vs. ES Modules (ESM)

JavaScript uses modules to split code across multiple files and avoid polluting the global scope.

CommonJS (CJS)

CommonJS is the traditional module system used heavily in Node.js.

It uses:

  • require() for importing
  • module.exports for exporting
// greet.js
module.exports = function greet(name) {
    return `Hello, ${name}`;
};
Enter fullscreen mode Exit fullscreen mode

Importing it:

const greet = require("./greet.js");

console.log(greet("Harshith"));
Enter fullscreen mode Exit fullscreen mode

CommonJS module loading is generally synchronous.

ES Modules (ESM)

ES Modules are the modern JavaScript module standard and are supported by both browsers and modern Node.js.

They use:

  • export
  • export default
  • import
// greet.js
export function greet(name) {
    return `Hello, ${name}`;
}
Enter fullscreen mode Exit fullscreen mode

Importing:

import { greet } from "./greet.js";

console.log(greet("Harshith"));
Enter fullscreen mode Exit fullscreen mode

ESM uses static module declarations, allowing JavaScript engines and tooling to analyze module dependencies before execution.

Named Export vs. Default Export

Feature Named Export Default Export
Number per file Multiple One
Import syntax {} required No {}
Import name Must match exported name Can be any name
Example import { add } from "./math.js" `import

Calculator from "./Calculator.js"` |

For example:

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

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

Importing:

import { add, subtract } from "./math.js";
Enter fullscreen mode Exit fullscreen mode

A default export:

export default function User() {
    // ...
}
Enter fullscreen mode Exit fullscreen mode

can be imported as:

import User from "./User.js";
Enter fullscreen mode Exit fullscreen mode

Important: Named imports use curly braces because they refer to specific named exports. They are not simply the same thing as ordinary object destructuring.


2. Garbage Collection, Closures, and Memory Leaks

JavaScript automatically manages memory using garbage collection.

Garbage Collection

A common model used by JavaScript engines is Mark-and-Sweep.

The engine starts from reachable roots, such as global references, and follows references to determine which objects are still reachable.

Objects that are no longer reachable can eventually be removed from memory.

let user = {
    name: "Harshith"
};

user = null;
Enter fullscreen mode Exit fullscreen mode

After assigning null, the object previously referenced by user may become unreachable and eligible for garbage collection, assuming nothing else references it.

Closures

A closure occurs when an inner function retains access to variables from its outer lexical scope even after the outer function has finished executing.

function createCounter() {
    let count = 0;

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

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
Enter fullscreen mode Exit fullscreen mode

Even though createCounter() has finished executing, the returned function still has access to count.

Memory Leaks

A memory leak occurs when data that is no longer logically needed remains reachable, preventing garbage collection.

Common causes include:

  • Uncleared timers
  • Event listeners that are never removed
  • Long-lived references to large objects
  • Closures that unintentionally retain large data

For example:

function setupListener() {
    const heavyData = new Array(1000000).fill("📦");

    document.getElementById("btn").addEventListener("click", () => {
        console.log("Clicked!");
    });
}
Enter fullscreen mode Exit fullscreen mode

The important lesson is that closures themselves are not memory leaks. A closure becomes a problem when it keeps references alive longer than necessary.


3. Four Essential Design Patterns

Design patterns are reusable approaches for solving common software design problems.

A. Module Pattern

The Module Pattern encapsulates internal state and exposes only the functionality that should be public.

A closure can be used to create private state:

const BankModule = (function () {

    let balance = 1000;

    return {
        withdraw(amount) {
            balance -= amount;
            return balance;
        }
    };

})();
Enter fullscreen mode Exit fullscreen mode

Here, balance cannot be accessed directly from outside the module.

Analogy: An ATM hides the internal banking system while exposing only operations such as withdraw and check balance.


B. Singleton Pattern

The Singleton Pattern ensures that only one instance of a particular object is used.

class DatabaseConnection {

    constructor() {

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

        this.connectionString = "mongodb://localhost:27017";

        DatabaseConnection.instance = this;
    }
}

const db1 = new DatabaseConnection();
const db2 = new DatabaseConnection();

console.log(db1 === db2); // true
Enter fullscreen mode Exit fullscreen mode

Both variables refer to the same instance.

Analogy: Think of a single shared resource that the application should coordinate through one instance.


C. Factory Pattern

The Factory Pattern centralizes object creation.

Instead of spreading object-creation logic throughout the application, a factory decides which object to create.

class UserFactory {

    static createUser(type, name) {

        if (type === "admin") {
            return new User(name, ["READ", "WRITE"]);
        }

        if (type === "guest") {
            return new User(name, ["READ"]);
        }
    }
}

const admin = UserFactory.createUser("admin", "Sarah");
Enter fullscreen mode Exit fullscreen mode

The caller doesn't need to know all the details required to create each type of user.

Analogy: A fast-food kitchen receives your order and handles the details of assembling the meal.


D. Observer Pattern

The Observer Pattern creates a one-to-many relationship where subscribers are notified when an event occurs.

class EventEmitter {

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

    on(event, listener) {

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

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

    emit(event, data) {

        if (this.events[event]) {
            this.events[event].forEach(fn => fn(data));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Multiple listeners can subscribe to an event:

Event occurs
     ↓
EventEmitter
     ↓
 ┌───┼───┐
 ↓   ↓   ↓
L1  L2  L3
Enter fullscreen mode Exit fullscreen mode

This helps create loosely coupled systems.


4. Functional Strategy Pattern

The Strategy Pattern allows us to choose between different algorithms without changing the main logic that uses them.

In functional JavaScript, strategies can simply be functions.

const fedexStrategy = (pkg) => {
    return pkg.weight * 1.5 + 5;
};

const uspsStrategy = (pkg) => {
    return pkg.weight * 0.9 + 3;
};
Enter fullscreen mode Exit fullscreen mode

We can create a function that accepts a strategy:

const createShippingCalculator = (strategy) => (pkg) => {
    return strategy(pkg);
};
Enter fullscreen mode Exit fullscreen mode

Then create a pre-configured calculator:

const calculateFedex =
    createShippingCalculator(fedexStrategy);

console.log(
    calculateFedex({ weight: 10 })
);
Enter fullscreen mode Exit fullscreen mode

The important idea is:

Shipping Calculator
       ↓
   Strategy
   ↙     ↘
FedEx    USPS
Enter fullscreen mode Exit fullscreen mode

We can change the strategy without changing the calculator itself.

Understanding (strategy) => (pkg) => ...

This syntax means that the outer function returns another function.

const createShippingCalculator = (strategy) => (pkg) => {
    return strategy(pkg);
};
Enter fullscreen mode Exit fullscreen mode

Conceptually:

const createShippingCalculator = (strategy) => {

    return (pkg) => {
        return strategy(pkg);
    };

};
Enter fullscreen mode Exit fullscreen mode

The returned function remembers strategy through a closure.


5. The SOLID Principles

SOLID is a collection of five principles that help us design maintainable and extensible software.

S — Single Responsibility Principle (SRP)

A class or function should have one clear responsibility and one main reason to change.

Instead of:

class User {
    saveToDatabase() {}
    sendEmail() {}
    generateReport() {}
}
Enter fullscreen mode Exit fullscreen mode

we can separate responsibilities:

class UserRepository {

    save(user) {
        // Save user
    }
}

class EmailService {

    sendWelcomeEmail(email) {
        // Send email
    }
}
Enter fullscreen mode Exit fullscreen mode

Each class focuses on a specific responsibility.


O — Open/Closed Principle (OCP)

Software should be open for extension but closed for modification.

For example:

class CreditCardPayment {

    pay(amount) {
        console.log(`Paid $${amount} with Credit Card`);
    }
}

class PayPalPayment {

    pay(amount) {
        console.log(`Paid $${amount} with PayPal`);
    }
}

class PaymentProcessor {

    process(amount, paymentStrategy) {
        paymentStrategy.pay(amount);
    }
}
Enter fullscreen mode Exit fullscreen mode

We can add another payment strategy without modifying PaymentProcessor.

PaymentProcessor
       ↓
   Strategy
   ↙  ↓  ↘
Card PayPal UPI
Enter fullscreen mode Exit fullscreen mode

L — Liskov Substitution Principle (LSP)

A subclass should be usable wherever its parent type is expected without breaking the application's behavior.

A problematic design would be:

class Bird {
    fly() {}
}

class Ostrich extends Bird {
    fly() {
        throw new Error("Cannot fly");
    }
}
Enter fullscreen mode Exit fullscreen mode

The parent class promises that birds can fly, but Ostrich cannot satisfy that expectation.

A better hierarchy is:

class Bird {}

class FlyingBird extends Bird {

    fly() {
        return "Flying";
    }
}

class Duck extends FlyingBird {}

class Ostrich extends Bird {}
Enter fullscreen mode Exit fullscreen mode

Now Ostrich doesn't inherit behavior it cannot support.


I — Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they don't need.

The general idea is:

Prefer small, focused interfaces or contracts rather than one large interface containing everything.

JavaScript doesn't have traditional interfaces like some languages, but the principle still applies when designing classes, objects, and APIs.


D — Dependency Inversion Principle (DIP)

High-level modules should not be tightly coupled to specific low-level implementations.

Instead, dependencies can be provided from outside.

class OrderProcessor {

    constructor(database) {
        this.db = database;
    }

    process(order) {
        this.db.save(order);
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, OrderProcessor doesn't create the database itself.

The dependency is injected:

const database = new Database();

const processor = new OrderProcessor(database);
Enter fullscreen mode Exit fullscreen mode

This makes the code easier to test and replace with another implementation.


javascript #webdevelopment #programming #softwareengineering #designpatterns #solidprinciples #nodejs #learninginpublic

Top comments (0)