Skip to content
JavaScript Design Patterns Explained — Module, Singleton, Observer & More

JavaScript Design Patterns Explained — Module, Singleton, Observer & More

DodaTech Updated Jun 15, 2026 8 min read

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]
  
Prerequisites: You should understand JavaScript objects, closures, and ES6 module syntax. Familiarity with OOP concepts is helpful.

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 thisbus.on('event', this.handleEvent.bind(this)) or use an arrow function.

Practice Questions

  1. 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.

  2. 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.

  3. 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.

  4. 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 is the most important design pattern in JavaScript?
The Module pattern — it’s the foundation of all JavaScript code organization. Every developer should understand closures and module patterns before exploring others.
Is Singleton an anti-pattern?
Not always, but it’s overused. Singletons make testing difficult and create hidden dependencies. Consider dependency injection first.
What’s the difference between Observer and Pub/Sub?
Observer is a one-to-many relationship between a subject and its observers. Pub/Sub adds a message broker between publishers and subscribers, making them completely decoupled.
Should I use classes for design patterns?
Patterns are language-agnostic concepts. In JavaScript, many patterns work better with functions and closures than with classes.
Can I use multiple patterns together?
Yes — patterns compose naturally. A Factory might create Singleton instances. An Observer might use the Module pattern for organization.

What’s Next

LessonDescription
JavaScript HomeBack 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 PatternsGeneral 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