JavaScript Design Patterns Explained — Module, Singleton, Observer & More
JavaScript design patterns are reusable solutions to common software design problems — battle-tested templates that help you write cleaner, more maintainable code. The Doda Browser uses the Observer pattern for its event system and the Module pattern to organize its codebase into maintainable units.
What You’ll Learn
- Module pattern — public/private members and ES6 modules
- Singleton — when one instance is enough (and when it isn’t)
- Observer — the publish/subscribe pattern for event-driven code
- Factory — creating objects without specifying concrete classes
- Prototype — object creation based on a template object
- Proxy — intercepting and customizing operations on objects
- Decorator — adding behavior to objects dynamically
Why Design Patterns Matter
Design patterns give you a shared vocabulary with other developers. When you say “we need an Observer pattern for the notification system,” every experienced developer knows exactly what you mean. The Durga Antivirus Pro team uses the Observer pattern to notify the UI when a threat is detected, the Factory pattern to create different scanner types, and the Proxy pattern to add logging around security operations.
Learning Path
flowchart LR
A[JS Objects & Prototypes] --> B[JS Modules]
B --> C[Design Patterns]
C --> D[Advanced JS]
C --> E[You Are Here]
Module Pattern
The Module pattern keeps related code together and controls what’s exposed to the global scope:
// Before ES6 — IIFE-based module pattern
const CounterModule = (function() {
let count = 0; // Private variable
function log(message) { // Private function
console.log(`[Counter] ${message}`);
}
return {
increment() { // Public method
count++;
log(`Count is now ${count}`);
},
decrement() {
count--;
log(`Count is now ${count}`);
},
getCount() {
return count;
}
};
})();
CounterModule.increment(); // [Counter] Count is now 1
CounterModule.increment(); // [Counter] Count is now 2
console.log(CounterModule.count); // undefined — private!
How it works: The IIFE (Immediately Invoked Function Expression) creates a closure. Variables inside it are private — only what the returned object exposes is accessible from outside.
ES6 Module Pattern
Modern JavaScript uses native modules with import/export:
// counter.js
let count = 0;
export function increment() { count++; }
export function decrement() { count--; }
export function getCount() { return count; }
// app.js
import { increment, getCount } from './counter.js';
increment();
console.log(getCount()); // 1
ES6 modules are the standard for modern JavaScript. Use the IIFE pattern only when you can’t use ES6 modules.
Singleton Pattern
Singleton ensures a class has only one instance and provides a global point of access:
class ConfigManager {
constructor() {
if (ConfigManager.instance) {
return ConfigManager.instance;
}
this.config = {};
ConfigManager.instance = this;
}
set(key, value) {
this.config[key] = value;
}
get(key) {
return this.config[key];
}
}
const config1 = new ConfigManager();
const config2 = new ConfigManager();
console.log(config1 === config2); // true — same instance
When to use: Application configuration, logging, database connections. When to avoid: Singleton makes testing harder because it introduces global state. In many cases, dependency injection is a better alternative.
Observer Pattern
The Observer pattern (also called Publish/Subscribe) lets objects subscribe to events and get notified when those events happen:
class EventBus {
constructor() {
this.listeners = {};
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event].push(callback);
return () => this.off(event, callback); // Return unsubscribe function
}
off(event, callback) {
this.listeners[event] = this.listeners[event]
.filter(cb => cb !== callback);
}
emit(event, data) {
(this.listeners[event] || []).forEach(cb => cb(data));
}
}
// Usage
const bus = new EventBus();
const unsubscribe = bus.on('user:login', (user) => {
console.log(`Welcome back, ${user.name}!`);
});
bus.emit('user:login', { name: 'Alice' });
// Output: Welcome back, Alice!
unsubscribe(); // Remove listener
bus.emit('user:login', { name: 'Bob' }); // No output
Observable Pattern for State Management
A more specialized version — the Observable — is used in libraries like Vue and MobX:
class Observable {
constructor(value) {
this._value = value;
this._subscribers = new Set();
}
get value() {
return this._value;
}
set value(newValue) {
if (this._value !== newValue) {
this._value = newValue;
this._subscribers.forEach(cb => cb(newValue));
}
}
subscribe(callback) {
this._subscribers.add(callback);
return () => this._subscribers.delete(callback);
}
}
const state = new Observable({ count: 0 });
state.subscribe((newVal) => console.log('State changed:', newVal));
state.value = { count: 1 }; // Output: State changed: { count: 1 }
Real-world use: The Doda Browser uses the Observer pattern internally to notify components when the user navigates to a new page, when downloads complete, or when bookmarks change.
Factory Pattern
Factory creates objects without exposing the instantiation logic:
class UserValidator {
validate(data) { /* ... */ }
}
class AdminValidator {
validate(data) { /* ... */ }
}
class GuestValidator {
validate(data) { /* ... */ }
}
function validatorFactory(role) {
switch (role) {
case 'admin': return new AdminValidator();
case 'user': return new UserValidator();
case 'guest': return new GuestValidator();
default: throw new Error(`Unknown role: ${role}`);
}
}
const validator = validatorFactory('admin');When to use: When creating objects involves complex logic, or when you want to centralize object creation so it’s easy to change later.
Prototype Pattern
JavaScript uses prototypal inheritance natively, but the Prototype pattern explicitly creates objects from a prototype:
const carPrototype = {
init(model, year) {
this.model = model;
this.year = year;
return this;
},
getInfo() {
return `${this.model} (${this.year})`;
}
};
const car1 = Object.create(carPrototype).init('Model X', 2024);
const car2 = Object.create(carPrototype).init('Model Y', 2025);
console.log(car1.getInfo()); // Model X (2024)
console.log(car1.__proto__ === carPrototype); // true
Proxy Pattern
Proxy wraps an object to intercept operations — useful for logging, validation, and lazy loading:
const target = { name: 'Alice' };
const handler = {
get(obj, prop) {
console.log(`Accessing property "${prop}"`);
return prop in obj ? obj[prop] : undefined;
},
set(obj, prop, value) {
console.log(`Setting "${prop}" to "${value}"`);
obj[prop] = value;
return true;
}
};
const proxy = new Proxy(target, handler);
proxy.name; // Logs: Accessing property "name"
proxy.age = 25; // Logs: Setting "age" to "25"
Decorator Pattern
Decorator adds behavior to an object without changing its class. In JavaScript, this is often done with higher-order functions:
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with:`, args);
const result = fn.apply(this, args);
console.log(`Result:`, result);
return result;
};
}
function withRetry(fn, maxAttempts = 3) {
return async function(...args) {
for (let i = 0; i < maxAttempts; i++) {
try {
return await fn.apply(this, args);
} catch (err) {
if (i === maxAttempts - 1) throw err;
console.log(`Attempt ${i + 1} failed, retrying...`);
}
}
};
}
// Compose decorators
const fetchWithRetryAndLogging = withLogging(
withRetry(fetchUser, 3)
);Common Mistakes
1. Using Singleton when you should use dependency injection
Singleton makes global state that’s hard to test. Prefer passing dependencies explicitly.
2. Creating too many event listeners in Observer
// Memory leak! Event listeners accumulate
button.addEventListener('click', handler);Fix: Always unsubscribe/remove listeners when they’re no longer needed.
3. Over-engineering with patterns
Not every problem needs a pattern. Start simple, add patterns when you need them — not before.
4. Using IIFE modules when ES6 modules work
// Old way (don't do this if you have a build step)
const mod = (function() { ... })();Fix: Use import/export with modern tooling.
5. Forgetting that this changes in Observers
class Component {
constructor() {
this.name = 'MyComponent';
bus.on('event', this.handleEvent); // `this` is wrong!
}
handleEvent() { console.log(this.name); }
}Fix: Bind this — bus.on('event', this.handleEvent.bind(this)) or use an arrow function.
Practice Questions
What’s the difference between Module and Singleton patterns? Module organizes related code with private/public members. Singleton ensures a class has only one instance. A module can contain a Singleton, but they solve different problems.
When would you use the Factory pattern? When object creation is complex, conditional, or likely to change. For example, creating different validator types based on user role.
What problem does the Observer pattern solve? It decouples the code that produces events from the code that reacts to them. Components can subscribe to events without knowing about each other.
Why is
Object.create()used in the Prototype pattern? It creates a new object with the specified prototype, letting you share methods without duplicating them in memory.
Challenge: Build a simple reactive state manager using the Observer pattern that supports nested state objects and computed properties.
Mini Project: Pub/Sub Chat Logger
class ChatLogger {
constructor() {
this.messages = [];
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event).push(callback);
return () => this.off(event, callback);
}
off(event, callback) {
const cbs = this.listeners.get(event);
if (cbs) {
this.listeners.set(event, cbs.filter(cb => cb !== callback));
}
}
emit(event, data) {
(this.listeners.get(event) || []).forEach(cb => cb(data));
}
send(user, message) {
const msg = { user, message, timestamp: Date.now() };
this.messages.push(msg);
this.emit('message', msg);
return msg;
}
}
const chat = new ChatLogger();
const unsub = chat.on('message', (msg) => {
console.log(`[${msg.user}]: ${msg.message}`);
});
chat.send('Alice', 'Hello!'); // [Alice]: Hello!
FAQ
What’s Next
| Lesson | Description |
|---|---|
| JavaScript Home | Back to the JavaScript hub |
| https://tutorials.dodatech.com/programming-languages/javascript/js-modules/ | Modules & dynamic imports |
| https://tutorials.dodatech.com/programming-languages/javascript/js-advanced/ | Advanced JavaScript topics |
| https://tutorials.dodatech.com/programming-languages/javascript/js-closures-scope/ | Closures & scope deep dive |
| Design Patterns | General design patterns overview |
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
What’s Next
Congratulations on completing this JavaScript Design Patterns tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro