Building a Notification System for SaaS Applications
A notification system for SaaS needs to handle multiple channels, user preferences, and tenant-level configuration without becoming an unmaintainable mess.
Strategic Systems Architect & Enterprise Software Developer
Notifications Are a System, Not a Feature
Early in a SaaS product's life, notifications start as scattered sendEmail() calls sprinkled throughout the codebase. Someone signs up — send a welcome email. An invoice is generated — send a receipt. A teammate is invited — send an invitation link. Each notification is implemented independently, usually by the developer working on the feature that triggers it.
This approach breaks down around the time you have 20 different notification types across three channels (email, in-app, push). Suddenly you have notification logic embedded in business logic throughout your codebase. Users can't control which notifications they receive. There's no way to see all notifications a user has received. Debugging why a notification wasn't delivered requires searching through multiple codebases.
Notifications deserve their own architecture — a centralized system that handles routing, channel selection, user preferences, delivery tracking, and retry logic. Building it right takes a few days of focused effort. Not building it costs far more in maintenance and debugging over time.
The Event-Driven Architecture
The cleanest notification architecture starts with events, not notifications. Instead of calling a notification service from your business logic, your business logic emits domain events — user.invited, invoice.generated, task.completed — and the notification system subscribes to those events and determines what notifications to send.
This separation has several important benefits. Your business logic doesn't know or care about notifications. Adding a new notification for an existing event requires no changes to the feature code. Disabling or modifying a notification doesn't touch any business logic. And the notification system has a single, clear responsibility.
The event-to-notification mapping defines which events trigger which notifications, through which channels, and to whom. For a task.completed event, the mapping might specify: send an in-app notification to the task assignor, send a push notification if they have push enabled, and send an email digest if they haven't seen the in-app notification within 2 hours.
This mapping is configuration, not code. Store it in a way that allows it to be modified without deployment — a database table or a configuration file that's loaded at startup. This makes it possible for product managers to adjust notification behavior without engineering involvement.
Channel Abstraction and Routing
Each notification channel (email, in-app, push, SMS, webhook) has different characteristics, delivery guarantees, and user expectations.
Email is asynchronous and persistent. Users expect it to arrive within minutes, not seconds. It's the right channel for information the user needs to reference later — receipts, reports, invitation links. Building solid email infrastructure is a prerequisite for reliable email notifications.
In-app notifications are real-time and ephemeral. They should appear instantly and be dismissable. They're the right channel for activity updates that the user cares about while actively using the product but doesn't need to reference later.
Push notifications interrupt the user's attention and should be used sparingly. They're appropriate for time-sensitive information — a deployment completed, an approval is needed, a security alert requires action. Overusing push notifications trains users to disable them entirely.
Webhooks are the notification channel for integrations. Enterprise customers want to receive events in their own systems — Slack, their internal tools, their data pipelines. Webhooks are technically notifications, and they benefit from the same architecture — routing, retry logic, and delivery tracking.
The abstraction layer between your notification system and these channels should present a uniform interface. Each channel implements send(notification, recipient), handles its own formatting and delivery, and reports delivery status back to the notification system. This makes adding a new channel (say, SMS or Microsoft Teams) a matter of implementing a new channel adapter, not modifying the core routing logic.
User Preferences and Tenant Configuration
Users must be able to control which notifications they receive and through which channels. This isn't optional — it's a basic product requirement and, for email, a legal one.
Per-notification-type preferences let users choose their desired channel for each notification type. "Send me task assignments via push and email. Send me weekly reports via email only. Don't send me comment notifications at all." The preference model should default to sensible choices but let users override at a granular level.
Notification frequency controls prevent notification fatigue. Some events happen in bursts — a batch import might trigger hundreds of record.created events. Your notification system needs debouncing logic that aggregates rapid-fire events into a single notification. "15 records were imported" is useful. 15 individual notifications are not.
Tenant-level configuration is essential for multi-tenant SaaS. Different tenants may want different notification defaults. An enterprise tenant might want all notifications to go through email for compliance reasons. A small team might prefer everything through in-app and push. Tenant administrators should be able to configure defaults that apply to all users within their organization, with individual users able to override within the bounds the administrator allows.
Quiet hours and scheduling let users suppress non-urgent notifications during specific time periods. The notification system queues notifications that arrive during quiet hours and delivers them when the quiet period ends. This requires timezone awareness and per-user schedule configuration.
Delivery Tracking and Reliability
Every notification should be tracked from creation through delivery. A notification record stores the event that triggered it, the recipient, the channel, the delivery status, and timestamps for each state transition. This data powers three critical capabilities.
First, debugging. When a user says "I never received that notification," you can look up exactly what happened — was the notification created? Was it routed to the right channel? Did the channel report a delivery failure? Was the user's preference set to suppress it?
Second, analytics. Which notifications have the highest engagement? Which are most frequently suppressed? This data informs product decisions about notification design and frequency.
Third, reliability. Failed deliveries should be retried with exponential backoff. Permanent failures (invalid email address, revoked push token) should trigger suppression to prevent repeated failure. The notification system should maintain its own health metrics — delivery success rate, average delivery latency, queue depth — with alerting for anomalies.
A well-built notification system is infrastructure that serves the product for years. The investment in getting the architecture right early compounds as your product grows and notification complexity increases.