Back to Engineering Notes
System desing - LLD/Oct 31, 2026/5 min read

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.

AKS

Amit Kumar Singh

Backend Engineer

I write about the production lessons behind backend systems, queues, databases, cloud, and AI products.

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.

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 Outcome


1. 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: UUID

  • type: (e.g., ORDER_CONFIRMATION, PASSWORD_RESET)

  • recipient_id: User ID or other recipient identifier

  • channel_preferences: Preferred channels (e.g., ['EMAIL', 'SMS'])

  • template_id: Reference to a template

  • payload: Key-value pairs for template variables

  • status: (PENDING, PROCESSING, SENT, FAILED, CANCELED)

  • created_at, scheduled_at, processed_at

2. Template

Defines the structure and content of a notification.

  • id: UUID

  • name: Unique name (e.g., order_confirmation_email)

  • channels: List of channels this template supports

  • content: (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 entity

  • notification_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: UUID

  • notification_id: Foreign key to Notification

  • channel: Channel used for delivery

  • provider_response_id: ID from external provider (e.g., Twilio SID)

  • status: (SUCCESS, FAILED, RETRYING)

  • error_details: Error code, message from provider

  • attempt_count: Number of retries

  • timestamp: 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 Template based on notification_type.

  • It retrieves UserPreferences for the recipient_id and notification_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.