HardSystem design

How do you design a notification system?

Technology:
System Design
Experience:
Senior

Quick answer

Accept notification requests through an API, put them on a message queue, and let channel-specific workers (email, SMS, push, in-app) send them using user preferences, templates, retries with idempotency and rate limits.

Problem

Design a service that sends email, SMS, mobile push and in-app notifications for a large product.

Functional requirements

  • Other services can trigger a notification for one user or a list of users.
  • Support email, SMS, push and in-app channels.
  • Respect user preferences and opt-outs per channel and category.
  • Support templates with variables and multiple languages.
  • Allow scheduled notifications and track delivery status.

Non-functional requirements

  • Scalability: handle spikes such as a marketing campaign to millions of users.
  • Availability: accepting requests must keep working even if a provider is down.
  • Performance: transactional messages (password reset) delivered within seconds.
  • Reliability: at-least-once delivery with de-duplication so users are not notified twice.

High-level architecture

  • Producers call a Notification API (or publish an event). The API validates the request, stores a notification record and enqueues it.
  • A routing worker loads user preferences and contact details, renders templates and fans out one message per channel onto channel-specific queues.
  • Channel workers call external providers (email service, SMS gateway, APNs/FCM) and record the result. Failed sends are retried with exponential backoff and moved to a dead-letter queue after several attempts.
  • In-app notifications are written to a store the client reads, and pushed in real time over WebSockets when the user is online.

Main components

API
Validates requests, applies authentication and stores the notification with an idempotency key.
Message queue
Buffers work and absorbs spikes; separate queues per channel and priority so bulk campaigns do not delay password resets.
Workers
Stateless consumers per channel that can be scaled independently.
Database
Notification records, delivery status, templates and user preferences.
Cache
User preferences and templates, which are read on every send.
Rate limiter
Per user (avoid spamming) and per provider (stay within quotas).

Database design

  • notifications(id, user_id, category, payload, idempotency_key UNIQUE, created_at, scheduled_at)
  • deliveries(id, notification_id, channel, status, provider_message_id, attempts, updated_at)
  • user_preferences(user_id, category, channel, enabled)
  • templates(id, category, channel, locale, subject, body, version)

Scaling

  • Scale channel workers horizontally based on queue depth.
  • Partition queues by priority; transactional messages get their own high-priority queue.
  • Batch bulk sends where providers support it.
  • Archive old delivery records to cheaper storage; keep recent ones for support and analytics.

Trade-offs

  • At-least-once delivery is simpler and more reliable than exactly-once; idempotency keys and provider message ids handle duplicates.
  • A queue adds latency and operational cost, but protects callers from slow or failing providers.
  • Storing every delivery gives auditability at the cost of storage; retention limits keep it manageable.

Discussion

A notification system sends messages to users across several channels when something happens in the product: an order ships, a password changes, a comment is posted. The hard parts are reliability, not losing or duplicating messages, and respecting user preferences at scale.

The core idea is to decouple producers from delivery. Product services publish an event; the notification service decides who should be told, through which channels, and hands each delivery to a queue so slow third-party providers never block the caller.

Key points

  • Decouple producers from delivery with queues
  • Separate queues per channel and priority
  • Retries with backoff, dead-letter queue and idempotency keys
  • Check user preferences before sending

Follow-up questions

  • System DesignMediumSystem design

    How do you design a rate limiter?

    Choose an algorithm such as token bucket or sliding window, store a counter per client (by user ID, API key or IP) in a fast shared store like Redis, and return HTTP 429 with a Retry-After header when the limit is exceeded.

    Mid-level 路 Senior 路 System Design
  • System DesignMedium

    What is idempotency in REST APIs and why does it matter?

    An operation is idempotent if repeating it any number of times has the same effect as doing it once; it matters because network retries can otherwise create duplicate orders or payments.

    Mid-level 路 Senior 路 REST API
  • System DesignHardSystem design

    How do you design a URL shortener like bit.ly?

    Generate a short unique code for each long URL (for example by base62-encoding a unique ID), store the mapping in a key-value or relational database, serve redirects through a cache because reads far outnumber writes, and record analytics asynchronously.

    Senior 路 System Design
  • System DesignHardSystem design

    How do you design a chat application like WhatsApp or Slack?

    Clients keep a persistent WebSocket connection to stateless chat servers; messages are stored durably, routed to the recipient鈥檚 server through a pub/sub layer, and delivered as push notifications when the recipient is offline.

    Senior 路 System Design
  • System DesignMedium

    When should you use a SQL database vs a NoSQL database?

    Choose SQL when your data is relational and you need transactions, joins and strong consistency; choose NoSQL when you need flexible schemas, very high write throughput or horizontal scaling for a specific access pattern.

    Mid-level 路 Senior 路 Database