DEV Community

Alex Chen
Alex Chen

Posted on

JavaScript Design Patterns Every Developer Should Know

JavaScript Design Patterns Every Developer Should Know

Patterns aren't just for enterprise Java. They solve real JS problems.

1. Singleton (One Instance Only)

// Class-based singleton
class Database {
  static #instance = null;

  constructor(config) {
    if (Database.#instance) return Database.#instance;
    this.config = config;
    this.connection = null;
    Database.#instance = this;
    return this;
  }

  async connect() {
    if (!this.connection) {
      this.connection = await createConnection(this.config);
    }
    return this.connection;
  }
}

// Usage: Always returns the same instance
const db1 = new Database({ host: 'localhost' });
const db2 = new Database({ host: 'localhost' });
console.log(db1 === db2); // true — same instance!

// Module-level singleton (simpler, preferred in Node.js)
// database.js
class Database {
  constructor() { /* ... */ }
}
module.exports = new Database(); // Single instance created at import time
Enter fullscreen mode Exit fullscreen mode

2. Observer / Event Emitter (Pub-Sub)

class EventEmitter {
  #listeners = {};

  on(event, callback) {
    (this.#listeners[event] ??= []).push(callback);
    return () => this.off(event, callback); // Unsubscribe function
  }

  off(event, callback) {
    const listeners = this.#listeners[event];
    if (!listeners) return;
    this.#listeners[event] = listeners.filter(cb => cb !== callback);
  }

  emit(event, ...args) {
    const listeners = this.#listeners[event];
    if (!listeners) return;
    listeners.forEach(callback => callback(...args));
  }

  once(event, callback) {
    const wrapper = (...args) => {
      callback(...args);
      this.off(event, wrapper);
    };
    this.on(event, wrapper);
  }
}

// Usage:
const store = new EventEmitter();

const unsub = store.on('state-changed', (newState, oldState) => {
  console.log(`State changed: ${oldState}${newState}`);
});

store.emit('state-changed', { count: 2 }, { count: 1 }); // Logs the change
unsub(); // Stop listening
store.emit('state-changed', { count: 3 }, { count: 2 }); // No log
Enter fullscreen mode Exit fullscreen mode

3. Factory (Create Objects Without Specifying Classes)

// UI component factory
function createUIComponent(type, props) {
  const components = {
    button: (props) => `<button class="${props.class || ''}">${props.label}</button>`,
    input: (props) => `<input type="${props.type || 'text'}" placeholder="${props.placeholder || ''}" />`,
    card: (props) => `
      <div class="card">
        <h3>${props.title}</h3>
        <p>${props.content}</p>
      </div>`,
    modal: (props) => `
      <dialog class="modal" open>
        <h2>${props.title}</h2>
        <p>${props.content}</p>
        <button onclick="this.closest('dialog').close()">Close</button>
      </dialog>
    `,
  };

  const factory = components[type];
  if (!factory) throw new Error(`Unknown component type: ${type}`);
  return factory(props);
}

// Usage:
const btn = createUIComponent('button', { label: 'Click me', class: 'primary' });
const form = createUIComponent('input', { type: 'email', placeholder: 'Email' });
Enter fullscreen mode Exit fullscreen mode

4. Strategy (Swap Algorithms at Runtime)

// Validation strategies
const validationStrategies = {
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  phone: (value) => /^\+?[\d\s-]{7,15}$/.test(value),
  password: (value) => value.length >= 8 && /[A-Z]/.test(value) && /[0-9]/.test(value),
  username: (value) => /^[a-zA-Z0-9_]{3,20}$/.test(value),
};

class Validator {
  constructor(strategy = 'email') {
    this.setStrategy(strategy);
  }

  setStrategy(type) {
    if (!validationStrategies[type]) {
      throw new Error(`Unknown strategy: ${type}`);
    }
    this.strategy = validationStrategies[type];
    return this; // Chainable!
  }

  validate(value) {
    const isValid = this.strategy(value);
    return { isValid, value };
  }
}

// Usage:
const validator = new Validator();

validator.setStrategy('email').validate('alex@example.com');   // { isValid: true }
validator.setStrategy('phone').validate('+1-555-1234567');     // { isValid: true }
validator.setStrategy('password').validate('abc');             // { isValid: false }
Enter fullscreen mode Exit fullscreen mode

5. Decorator (Add Behavior Without Modifying Original)

// Function decorators
function withLogging(fn) {
  return function(...args) {
    console.log(`Calling ${fn.name} with:`, args);
    const result = fn.apply(this, args);
    console.log(`${fn.name} returned:`, result);
    return result;
  };
}

function withRetry(fn, maxRetries = 3) {
  return async function(...args) {
    for (let i = 0; i <= maxRetries; i++) {
      try { return await fn.apply(this, args); } catch {}
    }
    throw new Error(`Failed after ${maxRetries + 1} attempts`);
  };
}

function withCache(fn, ttlMs = 5000) {
  const cache = new Map();
  return async function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      const { data, timestamp } = cache.get(key);
      if (Date.now() - timestamp < ttlMs) return data;
    }
    const data = await fn.apply(this, args);
    cache.set(key, { data, timestamp: Date.now() });
    return data;
  };
}

// Apply decorators
let fetchData = async (url) => {
  const res = await fetch(url);
  return res.json();
};

fetchData = withLogging(fetchData);
fetchData = withRetry(fetchData, 3);
fetchData = withCache(fetchData, 10000);

await fetchData('/api/users'); // Logged, retried on failure, cached for 10s
Enter fullscreen mode Exit fullscreen mode

6. Adapter (Make Incompatible Interfaces Work Together)

// Different payment providers have different APIs
class StripePaymentAdapter {
  constructor(stripeClient) {
    this.stripe = stripeClient;
  }

  async processPayment(amount, currency, token) {
    const result = await this.stripe.charges.create({
      amount: amount * 100, // Stripe uses cents
      currency,
      source: token,
    });
    return { success: result.paid, transactionId: result.id };
  }
}

class PayPalPaymentAdapter {
  constructor(paypalClient) {
    this.paypal = paypalClient;
  }

  async processPayment(amount, currency, token) {
    const order = await this.paypal.createOrder({
      intent: 'CAPTURE',
      purchase_units: [{ amount: { currency_code: currency, value: String(amount) } }],
    });
    // ... capture logic ...
    return { success: true, transactionId: order.id };
  }
}

// Unified interface
class PaymentProcessor {
  constructor(adapter) {
    this.adapter = adapter;
  }

  async pay(amount, currency, token) {
    return this.adapter.processPayment(amount, currency, token);
  }
}

// Swap adapters without changing application code:
const processor = new PaymentProcessor(new StripePaymentAdapter(stripeClient));
// Later: const processor = new PaymentProcessor(new PayPalPaymentAdapter(paypalClient));
await processor.pay(29.99, 'USD', 'tok_visa');
Enter fullscreen mode Exit fullscreen mode

7. Middleware Chain (Like Express!)

class MiddlewareChain {
  #middlewares = [];

  use(middleware) {
    this.#middlewares.push(middleware);
    return this; // Chainable
  }

  async execute(context) {
    let index = -1;

    const next = async () => {
      index++;
      if (index < this.#middlewares.length) {
        await this.#middlewares[index](context, next);
      }
    };

    await next();
    return context;
  }
}

// Usage:
const chain = new MiddlewareChain()
  .use(async (ctx, next) => {
    console.log('1. Before: Parse request');
    ctx.parsed = parseRequest(ctx.raw);
    await next();
    console.log('1. After: Request parsed');
  })
  .use(async (ctx, next) => {
    console.log('2. Before: Authenticate');
    ctx.user = await authenticate(ctx.parsed.token);
    await next();
    console.log('2. After: Authenticated');
  })
  .use(async (ctx, next) => {
    console.log('3. Handle request');
    ctx.response = await handleRequest(ctx.user, ctx.parsed);
    await next(); // No more middleware after this
  });

const result = await chain.execute({ raw: req });
Enter fullscreen mode Exit fullscreen mode

Which pattern do you use most? Any patterns I missed that are JS-specific?

Follow @armorbreak for more JavaScript content.

Top comments (0)