DEV Community

Cover image for Design Patterns You’ll Actually Use: A No-Nonsense Guide
Chisom Chima
Chisom Chima

Posted on

Design Patterns You’ll Actually Use: A No-Nonsense Guide

We have all been there. You start a project with the best intentions, but three months later, the codebase looks like a bowl of spaghetti. Changing one variable breaks five unrelated files, and "fixing" a bug feels like playing a dangerous game of Jenga.

This is exactly why design patterns exist. They aren't just academic theories meant to make you sound smart in interviews. They are battle-tested strategies for keeping your code clean and your sanity intact.

Here are the four patterns that every JavaScript developer should actually know.

The Singleton (The "Only One" Rule)

The Singleton is one of the simplest patterns to understand but also one of the most debated. The goal is simple: ensure a class has exactly one instance and provides a global point of access to it.

Think of a Database Connection or a Theme Manager. You don't want five different objects trying to manage your dark mode settings at the same time.

class ThemeManager {
  constructor() {
    if (ThemeManager.instance) {
      return ThemeManager.instance;
    }

    this.theme = 'light';
    ThemeManager.instance = this;
  }

  toggleTheme() {
    this.theme = this.theme === 'light' ? 'dark' : 'light';
    console.log(`Theme is now ${this.theme}`);
  }
}

// Even if we try to create a new one, we get the same instance
const managerA = new ThemeManager();
const managerB = new ThemeManager();

console.log(managerA === managerB); // true
Enter fullscreen mode Exit fullscreen mode

Pro Tip: Be careful with Singletons. They are essentially "global state," which can make testing a bit harder if you aren't careful. Use them only when you truly need a single source of truth.

The Factory Pattern (The Object Creator)

In a big app, you often need to create different types of objects based on a specific condition. Instead of cluttering your main logic with a dozen if/else or switch statements, you use a Factory.

Imagine you are building a notification system that handles Email, SMS, and Push notifications.

class Email {
  send(msg) { console.log(`Sending Email: ${msg}`); }
}

class SMS {
  send(msg) { console.log(`Sending SMS: ${msg}`); }
}

class NotificationFactory {
  createNotification(type) {
    switch(type) {
      case 'email': return new Email();
      case 'sms': return new SMS();
      default: return null;
    }
  }
}

const factory = new NotificationFactory();
const service = factory.createNotification('email');
service.send('Hello World!');
Enter fullscreen mode Exit fullscreen mode

This keeps your code "decoupled." The main part of your app doesn't need to know how an Email object is built; it just asks the factory for a notification service and goes to work.

The Observer Pattern (The Subscriber)

If you have ever used addEventListener in JavaScript, you have already used a version of the Observer pattern. It is all about "don't call me, I'll call you."

One object (the Subject) keeps a list of other objects (Observers) that want to know when something happens. When the state changes, the Subject broadcasts a message to everyone on the list.

class Store {
  constructor() {
    this.subscribers = [];
  }

  subscribe(fn) {
    this.subscribers.push(fn);
  }

  unsubscribe(fn) {
    this.subscribers = this.subscribers.filter(item => item !== fn);
  }

  broadcast(data) {
    this.subscribers.forEach(fn => fn(data));
  }
}

const myStore = new Store();

const logger = (data) => console.log(`Log: ${data}`);
const uiUpdater = (data) => console.log(`Updating UI with: ${data}`);

myStore.subscribe(logger);
myStore.subscribe(uiUpdater);

// When something happens, everyone gets the update
myStore.broadcast('New product added!');
Enter fullscreen mode Exit fullscreen mode

This is the backbone of state management libraries like Redux or the reactivity systems in Vue and React.

The Strategy Pattern (The Plugin Approach)

The Strategy pattern is a lifesaver when you have a specific task (like calculating a price) but there are multiple ways to do it. Instead of one massive function with a hundred arguments, you create separate "strategies."

Let’s look at a payment processing example:

// Different strategies
const paypal = (amount) => amount * 1.05; // 5% fee
const creditCard = (amount) => amount + 2; // Flat $2 fee
const crypto = (amount) => amount * 0.98; // 2% discount

class Order {
  constructor(amount) {
    this.amount = amount;
  }

  process(strategy) {
    return strategy(this.amount);
  }
}

const myOrder = new Order(100);
console.log(myOrder.process(paypal)); // 105
console.log(myOrder.process(crypto)); // 98
Enter fullscreen mode Exit fullscreen mode

The beauty here is that you can add a new payment method (like Apple Pay) just by writing a new small function. You don't have to touch the Order class at all.

The Reality Check: Don't Over-Engineer

Here is the most important advice I can give you: Don't use a pattern just to use a pattern.

I have seen developers turn a 10-line file into a 200-line "Pattern Masterpiece" that no one can read. If a simple if statement works and it's readable, stick with the if statement.

Patterns are tools for your belt. Use them when the code starts feeling heavy or hard to maintain.

What’s your favorite pattern?
Do you use these in your daily workflow, or do you think they add too much boilerplate? Let’s talk about it in the comments.

Top comments (2)

Collapse
 
dasheck0 profile image
Stefan Neidig

Matches at least my reality. I do use singleton more often than not, although it is considered an anti pattern. Observer pattern is a must have in modern flow architectures. And factory / strategy often come in handy. Everything else is nice and useful but is often not used (at least not in the fields where I work)

Collapse
 
bigprince profile image
Prince David

Learnt the observer pattern recently and your explanation just made me see it any way too, thanks!