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.exportsfor exporting
// greet.js
module.exports = function greet(name) {
return `Hello, ${name}`;
};
Importing it:
const greet = require("./greet.js");
console.log(greet("Harshith"));
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:
exportexport default-
import
// greet.js
export function greet(name) {
return `Hello, ${name}`;
}
Importing:
import { greet } from "./greet.js";
console.log(greet("Harshith"));
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;
}
Importing:
import { add, subtract } from "./math.js";
A default export:
export default function User() {
// ...
}
can be imported as:
import User from "./User.js";
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;
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
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!");
});
}
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;
}
};
})();
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
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");
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));
}
}
}
Multiple listeners can subscribe to an event:
Event occurs
↓
EventEmitter
↓
┌───┼───┐
↓ ↓ ↓
L1 L2 L3
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;
};
We can create a function that accepts a strategy:
const createShippingCalculator = (strategy) => (pkg) => {
return strategy(pkg);
};
Then create a pre-configured calculator:
const calculateFedex =
createShippingCalculator(fedexStrategy);
console.log(
calculateFedex({ weight: 10 })
);
The important idea is:
Shipping Calculator
↓
Strategy
↙ ↘
FedEx USPS
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);
};
Conceptually:
const createShippingCalculator = (strategy) => {
return (pkg) => {
return strategy(pkg);
};
};
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() {}
}
we can separate responsibilities:
class UserRepository {
save(user) {
// Save user
}
}
class EmailService {
sendWelcomeEmail(email) {
// Send email
}
}
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);
}
}
We can add another payment strategy without modifying PaymentProcessor.
PaymentProcessor
↓
Strategy
↙ ↓ ↘
Card PayPal UPI
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");
}
}
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 {}
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);
}
}
Here, OrderProcessor doesn't create the database itself.
The dependency is injected:
const database = new Database();
const processor = new OrderProcessor(database);
This makes the code easier to test and replace with another implementation.
Top comments (0)