Sending notifications — via email, SMS, push, or Slack — is a common requirement in most applications. But how do you design such a system so that it's easy to extend without modifying existing logic every time a new channel is added?

Enter the Factory Pattern and Open/Closed Principle.

In this article, I’ll walk you through how to build a flexible notification system using modern JavaScript, following clean architecture principles.

🧱 The Problem

Most developers start with something like this:

if (type === 'EMAIL') {
  sendEmail(message);
} else if (type === 'SMS') {
  sendSms(message);
} else if (type === 'PUSH') {
  sendPush(message);
}

This works — until your PM says, "We also need to support Slack. Oh, and WhatsApp next week."

Suddenly you're modifying the same block of logic again and again. That's not scalable.

The Goal

We want a system where:

Each notification type is its own class.

We can register new types without changing core logic.

The notification center can dynamically send messages based on type.

🏗️ Step-by-Step Implementation

  • Define Notification Classes
class EmailNotification {
  send(message) {
    console.log(`sending email notification with message ${message}`);
  }
}

class SmsNotification {
  send(message) {
    console.log(`sending sms notification with message ${message}`);
  }
}

class PushNotification {
  send(message) {
    console.log(`sending push notification with message ${message}`);
  }
}

class SlackNotification {
  send(message) {
    console.log(`sending slack notification with message ${message}`);
  }
}

Each class follows the same interface: a send() method that takes a message.

  • Create a Factory Base Class
class NotificationCreator {
  constructor() {
    this.registry = {
      'EMAIL': EmailNotification,
      'SMS': SmsNotification,
      'PUSH': PushNotification,
    };
  }

  register(type, notificationClass) {
    if (!this.registry[type]) {
      this.registry[type] = notificationClass;
    }
  }

  createNotification(type) {
    const NotificationClass = this.registry[type];
    if (!NotificationClass) {
      throw new Error(`${type} class is not implemented`);
    }
    return new NotificationClass();
  }
}

This class:

Holds a registry of notification types

Provides a register() method to add new types dynamically

Has a createNotification() method that acts as a factory

  • Notification Center That Sends Messages
class NotificationCenter extends NotificationCreator {
  send(message, type) {
    const notification = this.createNotification(type);
    notification.send(message);
  }
}
  • Putting It All Together
const notification = new NotificationCenter();

notification.send("Hey Email", "EMAIL");
notification.send("hey SMS", "SMS");
notification.send("hey PUSH-Notification", "PUSH");

// Register Slack dynamically
notification.register("SLACK", SlackNotification);
notification.send("Hey Slack", "SLACK");

🎯 Benefits of This Approach

✅ Open/Closed Principle: You can add new notification types without modifying existing logic.
✅ Scalable: Future types like WhatsApp, Telegram, Discord can be plugged in with one register() call.
✅ Testable: Each class can be tested independently.
✅ Clean Code: No repetitive if/else or switch blocks.

🧠 Bonus Tip: Make It Even Cleaner

Move the default registry into a method like registerDefaults() if your base class grows. Also, consider adding validation or interfaces if you're using TypeScript.

🚀 Final Thoughts

This pattern works great for:

Notification systems

Payment gateways (Stripe, Razorpay, PayPal)

Message formatters (Markdown, HTML, plain text)

By following this strategy, you're not only making your code cleaner — you're also writing software that's built to scale.

💬 Got questions or improvements? Drop them in the comments — let’s chat clean code!