Skip to content
⚠️ This article was written in 2019. Some content may be outdated.

Common JavaScript Design Patterns in Practice

Design patterns are not exclusive to backend development — they are used extensively in frontend code too. Mastering these patterns makes code more elegant and maintainable.

Singleton Pattern

Ensures a class has only one instance, globally sharing the same object. The most common frontend scenarios: global state management, modal management, authentication state.

javascript
// Implementation 1: closure
const Singleton = (function () {
  let instance;

  function createInstance(options) {
    return {
      name: options.name,
      log() {
        console.log(`Instance name: ${this.name}`);
      },
    };
  }

  return {
    getInstance(options) {
      if (!instance) {
        instance = createInstance(options);
      }
      return instance;
    },
  };
})();

const s1 = Singleton.getInstance({ name: "app" });
const s2 = Singleton.getInstance({ name: "other" });
console.log(s1 === s2); // true — always the same instance
javascript
// Implementation 2: ES6 class + static method
class ModalManager {
  static instance = null;

  constructor() {
    this.modals = [];
  }

  static getInstance() {
    if (!ModalManager.instance) {
      ModalManager.instance = new ModalManager();
    }
    return ModalManager.instance;
  }

  open(config) {
    const modal = { id: Date.now(), ...config, visible: true };
    this.modals.push(modal);
    return modal.id;
  }

  close(id) {
    const index = this.modals.findIndex((m) => m.id === id);
    if (index > -1) this.modals.splice(index, 1);
  }
}

Observer Pattern

javascript
class EventBus {
  constructor() {
    this.events = new Map();
  }

  on(event, handler) {
    if (!this.events.has(event)) this.events.set(event, new Set());
    this.events.get(event).add(handler);
    // Return unsubscribe function
    return () => this.off(event, handler);
  }

  off(event, handler) {
    this.events.get(event)?.delete(handler);
  }

  emit(event, data) {
    this.events.get(event)?.forEach((handler) => handler(data));
  }
}

const bus = new EventBus();
const unsubscribe = bus.on("user:login", (user) =>
  console.log("User logged in:", user),
);
bus.emit("user:login", { name: "Alice" });
unsubscribe(); // clean up

Strategy Pattern

javascript
// Without Strategy: large switch/if-else
function calculateDiscount(type, price) {
  if (type === "vip") return price * 0.8;
  if (type === "member") return price * 0.9;
  if (type === "newuser") return price - 20;
  return price;
}

// With Strategy: extensible, open for extension closed for modification
const discountStrategies = {
  vip: (price) => price * 0.8,
  member: (price) => price * 0.9,
  newuser: (price) => Math.max(0, price - 20),
  default: (price) => price,
};

function calculateDiscount(type, price) {
  const strategy = discountStrategies[type] || discountStrategies.default;
  return strategy(price);
}

// Adding a new discount type requires zero changes to existing code
discountStrategies.superVip = (price) => price * 0.7;

These three patterns — Singleton, Observer, and Strategy — cover the majority of real-world frontend scenarios. Master them and you'll write cleaner, more maintainable code.

MIT Licensed