Low-Level Design of a Robust Notification System
This article outlines a low-level design (LLD) for a robust notification system, focusing on core architectural components, key data models, notification flow, and practical engineering tradeoffs for backend and platform engineers.
This article outlines a low-level design (LLD) for a robust notification system, focusing on core architectural components, key data models, notification flow, and practical engineering tradeoffs for backend and platform engineers.
Amit Kumar Singh
Backend Engineer
I write about the production lessons behind backend systems, queues, databases, cloud, and AI products.
System desing - LLD
LLD for a Robust Notification System
Designing a Robust Notification System: An LLD Perspective
Building a reliable notification system is critical for user engagement and operational communication. This article outlines a low-level design (LLD) for such a system, focusing on core components, data models, and practical engineering tradeoffs for backend and platform engineers.
Core Architectural Components
A robust notification system typically comprises several distinct, interacting services or modules:
sequenceDiagram
actor Upstream Service as Upstream
participant Notification API as API
participant Message Queue as Queue
participant Notification Processor as Processor
participant Template Engine as Template
participant User Prefs Service as UserPrefs
participant Channel Adapter as Adapter
participant External Provider as Provider
participant Delivery Log as Log
Upstream->>API: Request Notification
API->>API: Validate & Store
API->>Queue: Enqueue Notification ID
Processor->>Queue: Consume Notification ID
Processor->>Template: Fetch Template
Processor->>UserPrefs: Get User Preferences
Processor->>Processor: Personalize & Route
Processor->>Adapter: Send to Channel
Adapter->>Provider: Deliver Notification
Provider-->>Adapter: Delivery Status
Adapter->>Log: Record Delivery Outcome1. Notification Request API
This is the entry point for internal and external services to send notifications. It should be stateless, validate incoming requests, and enqueue them for asynchronous processing.
Tradeoff: Direct synchronous sending versus asynchronous enqueuing. Asynchronous processing through a queue improves resilience and scalability, but introduces latency.
2. Message Queue
A durable message queue (e.g., Kafka, RabbitMQ, SQS) decouples the request API from processing. It handles backpressure and ensures notifications are not lost if downstream services are temporarily unavailable.
Consideration: At-least-once delivery guarantees are essential here.
3. Notification Processor Workers
These workers consume messages from the queue. Their responsibilities include:
Fetching notification templates.
Resolving user preferences (opt-in/out status for specific channels/types).
Personalizing notification content.
Routing notifications to appropriate channel adapters.
Scalability: Multiple workers can process messages in parallel, scaling horizontally based on load.
4. Template Engine
Manages notification templates (e.g., email HTML, SMS text, push notification payload structures). It allows for dynamic content injection using variables.
Design Choice: A dedicated service or an embedded library. A service offers centralized management and versioning, while an embedded library reduces latency.
5. Channel Adapters
These are specialized modules for sending notifications via specific channels:
Email: Integrates with SMTP providers (e.g., SendGrid, SES).
SMS: Integrates with SMS gateways (e.g., Twilio, Nexmo).
Push Notifications: Integrates with mobile platform services (e.g., FCM, APNS).
In-App/Webhooks: Direct API calls or websocket integrations.
Each adapter handles channel-specific logic, credential management, and error handling.
Tradeoff: Abstracting common adapter logic vs. optimizing for channel specifics. Abstraction aids extensibility, but might introduce overhead.
6. User Preference & Subscription Service
A dedicated service or module to manage user preferences for notification channels and types. It defines which notifications a user wishes to receive and via which channels.
Requirement: Fast lookup for processing workers.
7. Delivery Status Tracking & Logging
Records the outcome of each notification delivery attempt. This includes success, failure, error details, and timestamps. Essential for debugging, auditing, and analytics.
Data Store: A time-series database or a dedicated logging service might be suitable for high volumes.
8. Rate Limiting
Crucial for preventing abuse, managing costs with external providers, and respecting provider API limits. Can be applied per user, per channel, or globally.
Implementation: Distributed rate limiters (e.g., Redis-backed) are often necessary.
Key Data Models
Efficient data models underpin the system's functionality.
1. Notification
Represents an individual notification instance.
id: UUIDtype: (e.g.,ORDER_CONFIRMATION,PASSWORD_RESET)recipient_id: User ID or other recipient identifierchannel_preferences: Preferred channels (e.g.,['EMAIL', 'SMS'])template_id: Reference to a templatepayload: Key-value pairs for template variablesstatus: (PENDING,PROCESSING,SENT,FAILED,CANCELED)created_at,scheduled_at,processed_at
2. Template
Defines the structure and content of a notification.
id: UUIDname: Unique name (e.g.,order_confirmation_email)channels: List of channels this template supportscontent: (e.g., HTML for email, plain text for SMS)variables: Expected template variables (e.g.,{{order_id}},{{customer_name}})version: For template updates and A/B testing
3. UserPreference
Stores user-specific notification settings.
user_id: Foreign key to user entitynotification_type: (e.g.,ORDER_UPDATES,MARKETING)channel: (e.g.,EMAIL,SMS,PUSH)opt_in: Boolean (true for subscribed, false for unsubscribed)
4. DeliveryLog
Records the outcome of each delivery attempt.
id: UUIDnotification_id: Foreign key toNotificationchannel: Channel used for deliveryprovider_response_id: ID from external provider (e.g., Twilio SID)status: (SUCCESS,FAILED,RETRYING)error_details: Error code, message from providerattempt_count: Number of retriestimestamp: When the attempt occurred
Notification Flow
1. Request Ingestion: An upstream service calls the Notification Request API with notification_type, recipient_id, payload, and optionally channel_preferences.
2. Queueing: The API validates the request, creates a Notification record (status PENDING), and publishes a message to the Message Queue.
3. Processing: A Notification Processor Worker consumes the message.
It fetches the
Templatebased onnotification_type.It retrieves
UserPreferencesfor therecipient_idandnotification_type.It resolves the effective channels, prioritizing user preferences over request preferences.
It renders the template with the provided
payload.
4. Channel Routing: For each resolved channel, the worker routes the rendered content to the respective Channel Adapter.
5. Delivery Attempt: The Channel Adapter makes an API call to the external provider (e.g., SendGrid, Twilio), applying any necessary rate limits.
6. Status Logging: The result of the delivery attempt (success/failure, provider ID, error details) is recorded in the DeliveryLog. The Notification status is updated accordingly.
7. Error Handling & Retries: If a delivery fails, the system might enqueue a retry message (with exponential backoff) or move it to a Dead-Letter Queue (DLQ) for manual inspection.
Critical Design Considerations & Tradeoffs
Reliability (At-Least-Once Delivery): Use durable queues, idempotent operations, and robust retry mechanisms with exponential backoff and circuit breakers. Consider a DLQ for persistent failures.
Scalability: Decouple components with queues. Design stateless workers that can be horizontally scaled. Database choices should support high read/write throughput.
Extensibility: Design channel adapters with clear interfaces, making it easy to add new channels without modifying core logic. Template management should allow for easy addition of new notification types.
Observability: Implement comprehensive logging (structured logs for easier analysis), metrics (delivery rates, failure rates, latency per channel), and alerting for critical failures (e.g., queue backlogs, provider outages).
Security: Sanitize all input to prevent injection attacks (especially in templates). Securely manage API keys and credentials for external providers (e.g., using a secrets manager). Implement strict access control for the Notification Request API.
Cost Management: Monitor usage of external providers (SMS, email) closely. Implement effective rate limiting to prevent accidental overspending.
Idempotency: Ensure that processing a notification multiple times (due to retries or queue re-delivery) does not lead to duplicate notifications being sent to the user, or other undesirable side effects. This might involve tracking a unique request ID.
By carefully considering these LLD aspects and their tradeoffs, engineers can build a notification system that is not only functional but also resilient, scalable, and maintainable.
