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!
Top comments (0)