Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›System Design›Design Notification System

Classic Design Problems

Design Notification System

A notification system is a centralized infrastructure component designed to deliver asynchronous alerts to users via various channels like mobile push, email, or SMS. It is critical for maintaining user engagement, providing real-time updates, and ensuring high-availability communication across distributed applications. You reach for this pattern whenever your service needs to decouple message generation from delivery, handle massive scale, or manage cross-platform notification routing.

Core Architecture and Decoupling

The fundamental goal of any notification system is to decouple the business logic that triggers an event from the mechanisms required to deliver that event. In a monolithic approach, sending an email or push notification directly from a service blocks the request flow, causing latency and making the application fragile if the delivery provider experiences downtime. By introducing an asynchronous message queue, the producer service merely places a notification payload onto a queue and continues its execution. This architectural separation allows the system to absorb traffic spikes without overwhelming the target notification providers. We use a queue to buffer requests, ensuring that even if the downstream delivery services are slow or temporarily unreachable, messages remain persisted until they can be successfully dispatched by dedicated consumer workers. This pattern ensures that the system is resilient and capable of scaling independently based on the volume of incoming events.

# Simple producer-consumer structure using a message queue concept
class NotificationQueue:
    def __init__(self):
        self.queue = []  # Represents an in-memory buffer for messages

    def produce(self, message):
        self.queue.append(message)  # Business logic offloads task here
        return True

    def consume(self):
        return self.queue.pop(0) if self.queue else None  # Background workers pull tasks

Handling Channel-Specific Routing

Not all notifications are equal; they require specific routing logic based on the user's preferences, platform capabilities, and the urgency of the message. A robust notification system must maintain a template management service that stores the message structure, allowing developers to reuse formats across different channels without re-engineering the delivery pipeline. Furthermore, we must implement a routing layer that queries a user settings database to determine the preferred communication method for each recipient. If a user has disabled email notifications but enabled mobile push, the router must filter the request accordingly. By normalizing the input through an internal representation, we can easily extend the system to support new providers, like Slack or WhatsApp, by simply adding a new consumer service that translates our standardized internal format into the specific API requirements of the external service. This extensibility is vital for long-term platform maintenance.

def route_notification(user_prefs, message_template):
    # Routes messages based on channel priority and user configuration
    channels = ['push', 'email', 'sms']
    for channel in channels:
        if user_prefs.get(f'enable_{channel}'):
            # Send to specific worker pipeline for the channel
            print(f"Dispatching to {channel} pipeline for template: {message_template}")

Managing Rate Limiting and Backpressure

External notification providers often enforce strict rate limits to prevent abuse and ensure service quality. If we flood a third-party email provider with thousands of requests simultaneously, our account could be throttled or suspended. To mitigate this, our system must implement a rate-limiting mechanism at the worker level. By using a token bucket algorithm, we can control the flow of requests and distribute them over time. This approach allows us to handle bursts of events gracefully without breaching API limits. Furthermore, when an external service responds with a retryable error (such as a 429 Too Many Requests), our system must implement an exponential backoff strategy. Instead of immediately retrying and compounding the problem, we move the failed message back into the queue with a delayed visibility timeout, ensuring we do not overwhelm the external provider while maintaining the reliability of the notification delivery.

import time

def throttled_send(message, rate_limiter):
    # Uses token bucket to ensure we stay within API limits
    if rate_limiter.consume():
        return "Sent successfully"
    else:
        time.sleep(1) # Backpressure: wait before retry
        return "Rate limited, retrying"

Tracking and Delivery Guarantees

Reliability is defined by the ability to ensure that a notification is sent at least once. We achieve this by utilizing a database-backed storage layer to track the state of every message throughout its lifecycle, marking them as 'pending', 'sent', or 'failed'. When a worker retrieves a job from the queue, it performs a two-phase update: first to mark the status as 'processing' and eventually to 'sent' upon success. If a worker crashes before completion, the message will eventually reappear in the queue due to a timeout, allowing another worker to pick it up. This persistence layer is also essential for auditing and observability, providing insights into delivery success rates, latencies, and failed delivery patterns. By analyzing these logs, engineers can identify common failure points, such as invalid push tokens or server-side configuration issues, effectively improving the overall health of the notification system over time.

class MessageStore:
    def update_status(self, msg_id, status):
        # Simulates updating the status in a persistent database
        db = {'pending': [], 'sent': [], 'failed': []}
        db[status].append(msg_id)
        print(f"Message {msg_id} marked as {status}")

Security and Payload Privacy

Notification systems often carry sensitive user information, necessitating robust security measures throughout the delivery chain. Every request entering the system must be authenticated and validated to ensure that only authorized services can trigger notifications. Furthermore, the payload should be encrypted at rest and in transit. When constructing messages, we should avoid embedding PII (Personally Identifiable Information) directly into the push notification or email subject if it is not necessary. Instead, we can provide a secure deep link that redirects the user to the application, where they can authenticate and view the detailed content safely. By implementing strict validation logic on incoming event payloads, we protect our downstream workers from malformed data that could trigger vulnerabilities or system crashes. Treating every incoming notification request as potentially untrusted ensures the security of the broader ecosystem and protects against data leaks.

def sanitize_payload(payload):
    # Removes sensitive fields before queuing to prevent data leakage
    safe_data = {k: v for k, v in payload.items() if k != 'internal_user_id'}
    return safe_data # Only transmit sanitized data to downstream providers

Key points

  • Always decouple message generation from delivery using a message queue to prevent blocking the main application thread.
  • Implement a dedicated routing layer to respect user communication preferences and platform-specific channel capabilities.
  • Use rate limiting and exponential backoff to handle external provider constraints and avoid service throttling.
  • Maintain a persistent state store to track message lifecycle and ensure reliable at-least-once delivery guarantees.
  • Design the notification template engine to be reusable across multiple channels to maintain consistency and ease of maintenance.
  • Protect system integrity by validating all incoming notification requests and sanitizing payloads before processing.
  • Monitor delivery metrics to identify failed requests and optimize the performance of the overall messaging pipeline.
  • Support horizontal scaling by adding more consumer workers when the queue size exceeds predefined thresholds.

Common mistakes

  • Mistake: Coupling the notification service directly to the database. Why it's wrong: This creates high latency and blocking operations during high traffic. Fix: Use a message queue to decouple services and handle notifications asynchronously.
  • Mistake: Overlooking message deduplication. Why it's wrong: Network issues lead to retries, causing users to receive duplicate notifications. Fix: Implement an idempotency key and store message IDs in a distributed cache like Redis to filter duplicates.
  • Mistake: Sending all notifications synchronously in the main execution thread. Why it's wrong: This blocks the user application and causes massive system slowdowns. Fix: Use a worker-pool pattern where a producer pushes to a queue and consumers process messages in parallel.
  • Mistake: Ignoring notification preferences and throttling. Why it's wrong: Users get frustrated by too many alerts and may disable them entirely. Fix: Build a dedicated 'Notification Settings' service to manage user preferences and implement rate limiting per user/device.
  • Mistake: Storing all notification templates in code. Why it's wrong: Updating content requires a full deployment cycle. Fix: Store templates in a database or a configuration management system to allow real-time content changes.

Interview questions

What are the core requirements and functional components of a scalable notification system?

A notification system must support multi-channel delivery, including push, SMS, and email. The core components include a notification service that acts as an entry point, a data store for templates and user preferences, and message queues to decouple the producers from the consumers. We must ensure reliability through retries and delivery guarantees. The system needs to be scalable, fault-tolerant, and capable of handling millions of requests per day by using distributed workers.

How would you design the data flow for an asynchronous notification system?

To handle high traffic, we use an asynchronous flow. When a service triggers a notification, it publishes an event to a message queue like Kafka. A notification worker group consumes these events, fetches user preferences, and formats the payload. By decoupling the trigger from the delivery, we ensure the client gets a quick response. We use a database to store notification status, such as 'pending', 'sent', or 'failed', to facilitate monitoring and operational visibility for system administrators.

How can we ensure that notifications are delivered exactly once, or at least minimize duplicates?

Achieving exactly-once delivery is difficult in distributed systems due to network partitions. To minimize duplicates, we implement an idempotency key at the notification service level. Before processing, the worker checks a distributed cache like Redis to see if the event ID has been processed recently. If it exists, we discard the request. Additionally, we use database transactions when updating status records to ensure that duplicate processing does not lead to inconsistent states.

Compare using a pull-based polling mechanism versus a push-based WebSocket approach for real-time notifications.

Pull-based polling requires the client to repeatedly ask the server for updates, which wastes network bandwidth and battery life. It is simple to implement but lacks true real-time capabilities. Conversely, WebSockets establish a persistent, bidirectional connection, allowing the server to push updates instantly. While WebSockets are superior for real-time engagement, they are harder to scale because they keep connections open, requiring stateful load balancing and more memory on the server side compared to stateless HTTP polling.

How would you handle rate-limiting and notification prioritization in a high-volume system?

We implement rate-limiting at the gateway level using a token bucket algorithm to prevent spam and protect third-party providers. For prioritization, we categorize notifications into high-priority (e.g., security alerts) and low-priority (e.g., marketing). We maintain separate queues for these types and ensure that high-priority worker groups have more resources. We use a pattern like `PriorityQueue<Message>` where the consumer threads always poll the high-priority queue first to ensure critical system alerts bypass the marketing traffic backlog.

How do you handle system failures during the delivery process, and how does your monitoring strategy address this?

If a delivery fails, we implement an exponential backoff retry strategy. If the error is permanent (e.g., invalid phone number), we mark the status as 'failed' and stop retries. For transient issues, we move the event to a dead-letter queue (DLQ) for inspection. Monitoring involves tracking latency, delivery success rates, and queue depth. We use dashboards to alert on spikes in error rates, allowing us to proactively scale the worker pool or adjust rate limits dynamically to maintain system health.

All System Design interview questions →

Check yourself

1. When designing a notification system that must support millions of users, why is introducing a message queue between the notification service and the delivery workers essential?

  • A.To ensure the database is always updated before the user receives the message.
  • B.To buffer spikes in traffic and allow for asynchronous processing, preventing service crashes.
  • C.To guarantee that every message is delivered in the exact order it was generated.
  • D.To reduce the storage requirement of the Notification Template Service.
Show answer

B. To buffer spikes in traffic and allow for asynchronous processing, preventing service crashes.
Option 2 is correct because queues act as a shock absorber for bursty traffic. Option 1 is wrong because synchronous DB updates are the bottleneck we are trying to avoid. Option 3 is wrong because ordering is rarely required for notifications and complicates performance. Option 4 is wrong because queues do not affect storage.

2. Which strategy is most effective for preventing a user from receiving the same push notification multiple times due to a retry logic failure?

  • A.Increasing the timeout duration on the push gateway client.
  • B.Using a distributed lock to ensure only one thread reads the message from the queue.
  • C.Assigning a unique event ID to each notification and checking it against a cache before delivery.
  • D.Validating the user's login status before every notification attempt.
Show answer

C. Assigning a unique event ID to each notification and checking it against a cache before delivery.
Option 3 is correct as it implements idempotency via a unique ID, which is the industry standard. Option 1 doesn't solve duplication. Option 2 is inefficient for large scale. Option 4 is a security check, not a duplicate prevention mechanism.

3. How should a system handle the 'Rate Limiting' of notifications for a specific user?

  • A.By enforcing a global limit on the number of notifications sent across the entire platform.
  • B.By dropping all messages once a user reaches their daily quota.
  • C.By implementing a per-user bucket in a cache to track and limit notification frequency based on policy.
  • D.By asking the database to perform a count query before every single dispatch.
Show answer

C. By implementing a per-user bucket in a cache to track and limit notification frequency based on policy.
Option 3 is correct because Redis/distributed caches provide low-latency lookups for rate limiting. Option 1 is wrong because limits must be per-user, not global. Option 2 is poor UX. Option 4 is inefficient and would crash the database under load.

4. Why is it beneficial to separate the Notification Service into dedicated sub-services for different channels (e.g., Email, SMS, Push)?

  • A.It prevents the system from having a single point of failure and allows independent scaling.
  • B.It reduces the amount of total network traffic generated.
  • C.It allows all services to share the same database schema for consistency.
  • D.It eliminates the need for a message queue.
Show answer

A. It prevents the system from having a single point of failure and allows independent scaling.
Option 1 is correct because each channel has different latency and third-party API requirements, necessitating independent scaling. Option 2 is false as traffic volume remains the same. Option 3 is incorrect as schemas often differ per channel. Option 4 is false, as queues remain essential for throughput.

5. When storing 'Notification Templates', what is the main advantage of using an external template engine or database instead of hard-coding messages?

  • A.It improves the performance of the rendering engine.
  • B.It allows for dynamic content localization and quick content adjustments without code deployment.
  • C.It makes the system fully synchronous and easier to debug.
  • D.It ensures that users always receive messages in the exact order they were sent.
Show answer

B. It allows for dynamic content localization and quick content adjustments without code deployment.
Option 2 is correct because business teams often need to change marketing copy without developer intervention. Option 1 is not the primary benefit. Option 3 is false, as decoupling actually increases complexity. Option 4 has no relationship to templates.

Take the full System Design quiz →

← PreviousDesign Twitter / News FeedNext →Design Ride-Sharing (Uber)

System Design

31 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app