LLD Hub
lldobserverstrategytemplate-method

How to Design a Notification System | LLD Interview Guide

Design a multi-channel notification service (Email, SMS, Push, In-App) with rate limiting, retry logic, and user preferences. Common at Razorpay, PhonePe.

10 April 2025·8 min read

Practice this problem

Notification System — get AI-scored feedback on your solution

Solve it →

A Notification System that handles multiple channels (Email, SMS, Push, In-App) with rate limiting, retry, and user preferences is a frequent LLD problem at Razorpay, PhonePe, Freshworks, and other product companies. Let's design it end-to-end.

Core Entities

  • Notification — type, title, body, priority, recipient
  • Channel — Email, SMS, Push, InApp
  • UserPreference — which channels enabled per notification type
  • NotificationService — routes notifications to appropriate channels
  • RateLimiter — prevents channel flooding
  • RetryQueue — retries failed deliveries

Strategy Pattern for Channels

interface NotificationChannel {
  send(notification: Notification, user: User): boolean;
  supports(type: NotificationType): boolean;
}

class EmailChannel implements NotificationChannel {
  send(n: Notification, user: User): boolean {
    if (!user.email) return false;
    return this.emailClient.send(user.email, n.title, n.body);
  }
}
class SMSChannel   implements NotificationChannel { ... }
class PushChannel  implements NotificationChannel { ... }
class InAppChannel implements NotificationChannel { ... }

Routing by User Preference

class NotificationService {
  send(notification: Notification, userId: string): void {
    const prefs = this.prefRepo.get(userId, notification.type);
    const channels = this.channels.filter(c =>
      prefs.enabledChannels.includes(c.name) && c.supports(notification.type)
    );

    for (const channel of channels) {
      if (!this.rateLimiter.allow(userId, channel.name)) {
        if (notification.priority === Priority.CRITICAL) {
          this.bypassAndSend(channel, notification, userId); // Critical bypasses limits
        }
        continue;
      }
      const success = channel.send(notification, this.userRepo.get(userId));
      if (!success) this.retryQueue.enqueue(notification, channel, userId);
    }
  }
}

Rate Limiter

class RateLimiter {
  // Sliding window per user per channel
  allow(userId: string, channel: string): boolean {
    const key = `${userId}:${channel}`;
    const count = this.countInLastHour(key);
    const limit = this.getLimitForChannel(channel); // Email: 100/hr, SMS: 10/hr
    return count < limit;
  }
}

Retry with Exponential Backoff

class RetryQueue {
  enqueue(notification: Notification, channel: NotificationChannel, userId: string) {
    const job = { notification, channel, userId, attempts: 0 };
    this.schedule(job, 1000); // First retry in 1 second
  }
  private retry(job: RetryJob) {
    if (job.attempts >= 3) { this.dlq.add(job); return; }
    const success = job.channel.send(job.notification, this.userRepo.get(job.userId));
    if (!success) {
      job.attempts++;
      this.schedule(job, Math.pow(2, job.attempts) * 1000); // Exponential backoff
    }
  }
}

Ready to practice?

Submit your solution and get AI-scored feedback on OOP, SOLID principles, design patterns, and code quality.

Solve Notification System