Notification Best Practices
Implement these best practices to maximize the effectiveness of notifications while maintaining positive user experiences.
Channel Selection
Choose the appropriate channel based on message urgency and user preferences:
Channel | Best For | Not Recommended For |
---|---|---|
SMS | Time-sensitive alerts, 2FA codes | Marketing content, non-critical updates |
Detailed information, receipts, reports | Urgent alerts requiring immediate action | |
Interactive conversations, rich media | Unsolicited marketing messages | |
In-App | Context-specific updates, system status | Critical security alerts when app is closed |
Recommendation: Implement a preference center that allows users to select their preferred channels for different notification types.
Message Content
SMS
- Keep messages under 160 characters to avoid message splitting
- Include a clear call-to-action if applicable
- Identify your organization at the beginning of the message
- Use URL shorteners for any links
- C#
- Java
- Python
- JavaScript
// Good example
string goodSmsMessage = "Lightstone: Your verification code is 123456. Valid for 10 minutes. Do not share this code.";
// Poor example - too long, unclear sender
string poorSmsMessage = "Your one-time password for verifying your account on our platform is 123456. This code will expire in 10 minutes. Please enter it on the verification screen to complete your registration process. Thank you for using our service.";
// Good example
String goodSmsMessage = "Lightstone: Your verification code is 123456. Valid for 10 minutes. Do not share this code.";
// Poor example - too long, unclear sender
String poorSmsMessage = "Your one-time password for verifying your account on our platform is 123456. This code will expire in 10 minutes. Please enter it on the verification screen to complete your registration process. Thank you for using our service.";
# Good example
good_sms_message = "Lightstone: Your verification code is 123456. Valid for 10 minutes. Do not share this code."
# Poor example - too long, unclear sender
poor_sms_message = "Your one-time password for verifying your account on our platform is 123456. This code will expire in 10 minutes. Please enter it on the verification screen to complete your registration process. Thank you for using our service."
// Good example
const goodSmsMessage = "Lightstone: Your verification code is 123456. Valid for 10 minutes. Do not share this code.";
// Poor example - too long, unclear sender
const poorSmsMessage = "Your one-time password for verifying your account on our platform is 123456. This code will expire in 10 minutes. Please enter it on the verification screen to complete your registration process. Thank you for using our service.";
Email
- Use descriptive subject lines (4-7 words)
- Design for mobile-first viewing
- Include plain text alternatives to HTML
- Keep email width between 600-800 pixels
WhatsApp
- Follow WhatsApp Business Policy guidelines
- Obtain explicit opt-in before messaging
- Use templates for consistency
- Keep messages concise and conversational
Timing and Frequency
- Respect user time zones when scheduling notifications
- Implement rate limiting to prevent notification fatigue
- Group related notifications to reduce interruptions
- Allow users to snooze or temporarily disable notifications
- C#
- Java
- Python
- JavaScript
// Example rate limiting implementation
public class NotificationRateLimiter
{
private readonly Dictionary<string, int> _maxNotificationsPerHour = new Dictionary<string, int>
{
{ "sms", 2 },
{ "email", 5 },
{ "whatsapp", 3 },
{ "inApp", 10 }
};
public bool ShouldSendNotification(string userId, string channelType)
{
// Check if user has received max notifications for this channel
var recentNotifications = GetRecentNotifications(userId, channelType);
return recentNotifications.Count < _maxNotificationsPerHour[channelType];
}
private List<Notification> GetRecentNotifications(string userId, string channelType)
{
// Implementation to retrieve recent notifications from database
// This would query notifications from the last hour
return new List<Notification>();
}
}
// Example rate limiting implementation
public class NotificationRateLimiter {
private final Map<String, Integer> maxNotificationsPerHour = new HashMap<String, Integer>() {{
put("sms", 2);
put("email", 5);
put("whatsapp", 3);
put("inApp", 10);
}};
public boolean shouldSendNotification(String userId, String channelType) {
// Check if user has received max notifications for this channel
List<Notification> recentNotifications = getRecentNotifications(userId, channelType);
return recentNotifications.size() < maxNotificationsPerHour.get(channelType);
}
private List<Notification> getRecentNotifications(String userId, String channelType) {
// Implementation to retrieve recent notifications from database
// This would query notifications from the last hour
return new ArrayList<>();
}
}
# Example rate limiting implementation
class NotificationRateLimiter:
def __init__(self):
self._max_notifications_per_hour = {
"sms": 2,
"email": 5,
"whatsapp": 3,
"inApp": 10
}
def should_send_notification(self, user_id, channel_type):
# Check if user has received max notifications for this channel
recent_notifications = self._get_recent_notifications(user_id, channel_type)
return len(recent_notifications) < self._max_notifications_per_hour[channel_type]
def _get_recent_notifications(self, user_id, channel_type):
# Implementation to retrieve recent notifications from database
# This would query notifications from the last hour
return []
// Example rate limiting implementation
class NotificationRateLimiter {
constructor() {
this._maxNotificationsPerHour = {
'sms': 2,
'email': 5,
'whatsapp': 3,
'inApp': 10
};
}
shouldSendNotification(userId, channelType) {
// Check if user has received max notifications for this channel
const recentNotifications = this.getRecentNotifications(userId, channelType);
return recentNotifications.length < this._maxNotificationsPerHour[channelType];
}
getRecentNotifications(userId, channelType) {
// Implementation to retrieve recent notifications from database
// This would query notifications from the last hour
return [];
}
}
Error Handling
Implement robust error handling to ensure notification delivery:
- Retry Logic: Implement exponential backoff for failed notifications
- C#
- Java
- Python
- JavaScript
public class NotificationSender
{
public async Task<NotificationResult> SendWithRetry(Notification notification, int maxRetries = 3)
{
for (int attempt = 0; attempt <= maxRetries; attempt++)
{
try
{
return await SendNotification(notification);
}
catch (Exception ex)
{
if (attempt == maxRetries)
throw;
// Wait 2^attempt seconds before retrying
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)));
// Log retry attempt
_logger.LogWarning($"Retry attempt {attempt + 1} for notification {notification.Id}. Error: {ex.Message}");
}
}
// This line should not be reached, but required by compiler
throw new InvalidOperationException("Failed to send notification after retries");
}
private async Task<NotificationResult> SendNotification(Notification notification)
{
// Implementation of actual notification sending logic
return new NotificationResult();
}
}
public class NotificationSender {
private final Logger logger = LoggerFactory.getLogger(NotificationSender.class);
public CompletableFuture<NotificationResult> sendWithRetry(Notification notification, int maxRetries) {
return CompletableFuture.supplyAsync(() -> {
for (int attempt = 0; attempt <= maxRetries; attempt++) {
try {
return sendNotification(notification).get();
} catch (Exception ex) {
if (attempt == maxRetries) {
throw new RuntimeException("Max retries exceeded", ex);
}
// Wait 2^attempt seconds before retrying
try {
Thread.sleep((long) (Math.pow(2, attempt) * 1000));
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
// Log retry attempt
logger.warn("Retry attempt {} for notification {}. Error: {}",
attempt + 1, notification.getId(), ex.getMessage());
}
}
// This line should not be reached
throw new IllegalStateException("Failed to send notification after retries");
});
}
private CompletableFuture<NotificationResult> sendNotification(Notification notification) {
// Implementation of actual notification sending logic
return CompletableFuture.completedFuture(new NotificationResult());
}
}
import asyncio
import logging
import math
class NotificationSender:
def __init__(self):
self._logger = logging.getLogger(__name__)
async def send_with_retry(self, notification, max_retries=3):
for attempt in range(max_retries + 1):
try:
return await self._send_notification(notification)
except Exception as ex:
if attempt == max_retries:
raise
# Wait 2^attempt seconds before retrying
await asyncio.sleep(math.pow(2, attempt))
# Log retry attempt
self._logger.warning(f"Retry attempt {attempt + 1} for notification {notification.id}. Error: {str(ex)}")
# This line should not be reached
raise RuntimeError("Failed to send notification after retries")
async def _send_notification(self, notification):
# Implementation of actual notification sending logic
return NotificationResult()
class NotificationSender {
constructor(logger) {
this._logger = logger;
}
async sendWithRetry(notification, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await this.sendNotification(notification);
} catch (ex) {
if (attempt === maxRetries) {
throw ex;
}
// Wait 2^attempt seconds before retrying
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
// Log retry attempt
this._logger.warn(`Retry attempt ${attempt + 1} for notification ${notification.id}. Error: ${ex.message}`);
}
}
// This line should not be reached
throw new Error('Failed to send notification after retries');
}
async sendNotification(notification) {
// Implementation of actual notification sending logic
return new NotificationResult();
}
}
- Fallback Channels: Configure alternative channels if primary channel fails
- C#
- Java
- Python
- JavaScript
public class NotificationService
{
private readonly ISmsService _smsService;
private readonly IEmailService _emailService;
private readonly ILogger<NotificationService> _logger;
public NotificationService(ISmsService smsService, IEmailService emailService, ILogger<NotificationService> logger)
{
_smsService = smsService;
_emailService = emailService;
_logger = logger;
}
public async Task<DeliveryResult> SendWithFallback(NotificationRequest notification)
{
try
{
return await _smsService.SendNotification(notification);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "SMS delivery failed, attempting email fallback");
return await _emailService.SendNotification(notification);
}
}
}
public class NotificationService {
private final SmsService smsService;
private final EmailService emailService;
private final Logger logger;
public NotificationService(SmsService smsService, EmailService emailService, Logger logger) {
this.smsService = smsService;
this.emailService = emailService;
this.logger = logger;
}
public CompletableFuture<DeliveryResult> sendWithFallback(NotificationRequest notification) {
return smsService.sendNotification(notification)
.exceptionally(ex -> {
logger.warn("SMS delivery failed, attempting email fallback", ex);
try {
return emailService.sendNotification(notification).get();
} catch (Exception e) {
throw new CompletionException(e);
}
});
}
}
import logging
class NotificationService:
def __init__(self, sms_service, email_service):
self._sms_service = sms_service
self._email_service = email_service
self._logger = logging.getLogger(__name__)
async def send_with_fallback(self, notification):
try:
return await self._sms_service.send_notification(notification)
except Exception as ex:
self._logger.warning(f"SMS delivery failed, attempting email fallback: {str(ex)}")
return await self._email_service.send_notification(notification)
class NotificationService {
constructor(smsService, emailService, logger) {
this._smsService = smsService;
this._emailService = emailService;
this._logger = logger;
}
async sendWithFallback(notification) {
try {
return await this._smsService.sendNotification(notification);
} catch (ex) {
this._logger.warn(`SMS delivery failed, attempting email fallback: ${ex.message}`);
return await this._emailService.sendNotification(notification);
}
}
}
- Monitor Delivery Rates: Track delivery success rates across channels
- Log all notification attempts and outcomes
- Set up alerts for abnormal failure rates
- Review delivery metrics weekly to identify problem areas
Security Considerations
- Never include sensitive information in notifications
- Implement proper authentication for notification APIs
- Rotate API keys regularly
- Use TLS for all API communications
- Validate all input data before sending notifications
- C#
- Java
- Python
- JavaScript
public static class PhoneValidator
{
// Validate phone numbers before sending
public static bool ValidatePhoneNumber(string phoneNumber, string countryCode)
{
// Strip any non-numeric characters
string cleaned = Regex.Replace(phoneNumber, @"\D", "");
// Validate based on country code
var patterns = new Dictionary<string, string>
{
{ "ZA", @"^(0\d{9})$" },
{ "US", @"^\d{10}$" }
// Add other countries as needed
};
if (patterns.TryGetValue(countryCode, out string pattern))
{
return Regex.IsMatch(cleaned, pattern);
}
return false;
}
}
public class PhoneValidator {
// Validate phone numbers before sending
public static boolean validatePhoneNumber(String phoneNumber, String countryCode) {
// Strip any non-numeric characters
String cleaned = phoneNumber.replaceAll("\\D", "");
// Validate based on country code
Map<String, String> patterns = new HashMap<>();
patterns.put("ZA", "^(0\\d{9})$");
patterns.put("US", "^\\d{10}$");
// Add other countries as needed
if (patterns.containsKey(countryCode)) {
String pattern = patterns.get(countryCode);
return cleaned.matches(pattern);
}
return false;
}
}
import re
class PhoneValidator:
@staticmethod
def validate_phone_number(phone_number, country_code):
# Strip any non-numeric characters
cleaned = re.sub(r'\D', '', phone_number)
# Validate based on country code
patterns = {
"ZA": r"^(0\d{9})$",
"US": r"^\d{10}$"
# Add other countries as needed
}
if country_code in patterns:
pattern = patterns[country_code]
return bool(re.match(pattern, cleaned))
return False
class PhoneValidator {
// Validate phone numbers before sending
static validatePhoneNumber(phoneNumber, countryCode) {
// Strip any non-numeric characters
const cleaned = phoneNumber.replace(/\D/g, '');
// Validate based on country code
const patterns = {
'ZA': /^(0\d{9})$/,
'US': /^\d{10}$/
// Add other countries as needed
};
if (patterns[countryCode]) {
return patterns[countryCode].test(cleaned);
}
return false;
}
}
Testing Strategy
Test notifications thoroughly before production deployment:
- Sandbox Testing: Test in isolated environments with known test numbers/emails
- Template Validation: Verify template rendering across device types
- Load Testing: Ensure systems handle peak notification volumes
- End-to-end Testing: Test entire notification flow from trigger to delivery
- Compliance Testing: Verify notifications meet regulatory requirements
Create dedicated test accounts for each notification channel:
- C#
- Java
- Python
- JavaScript
public static class TestAccounts
{
public static readonly TestEnvironment Accounts = new TestEnvironment
{
SmsTestAccount = new SmsAccount
{
Number = "0000000000",
CountryCode = "ZA"
},
EmailTestAccount = "test-notifications@yourdomain.com",
WhatsAppTestAccount = new WhatsAppAccount
{
Number = "0000000000",
CountryCode = "ZA"
}
};
}
public class TestEnvironment
{
public SmsAccount SmsTestAccount { get; set; }
public string EmailTestAccount { get; set; }
public WhatsAppAccount WhatsAppTestAccount { get; set; }
}
public class SmsAccount
{
public string Number { get; set; }
public string CountryCode { get; set; }
}
public class WhatsAppAccount
{
public string Number { get; set; }
public string CountryCode { get; set; }
}
public class TestAccounts {
public static final TestEnvironment ACCOUNTS = new TestEnvironment(
new SmsAccount("0000000000", "ZA"),
"test-notifications@yourdomain.com",
new WhatsAppAccount("0000000000", "ZA")
);
}
public class TestEnvironment {
private final SmsAccount smsTestAccount;
private final String emailTestAccount;
private final WhatsAppAccount whatsAppTestAccount;
public TestEnvironment(SmsAccount smsTestAccount, String emailTestAccount,
WhatsAppAccount whatsAppTestAccount) {
this.smsTestAccount = smsTestAccount;
this.emailTestAccount = emailTestAccount;
this.whatsAppTestAccount = whatsAppTestAccount;
}
public SmsAccount getSmsTestAccount() { return smsTestAccount; }
public String getEmailTestAccount() { return emailTestAccount; }
public WhatsAppAccount getWhatsAppTestAccount() { return whatsAppTestAccount; }
}
public class SmsAccount {
private final String number;
private final String countryCode;
public SmsAccount(String number, String countryCode) {
this.number = number;
this.countryCode = countryCode;
}
public String getNumber() { return number; }
public String getCountryCode() { return countryCode; }
}
public class WhatsAppAccount {
private final String number;
private final String countryCode;
public WhatsAppAccount(String number, String countryCode) {
this.number = number;
this.countryCode = countryCode;
}
public String getNumber() { return number; }
public String getCountryCode() { return countryCode; }
}
class SmsAccount:
def __init__(self, number, country_code):
self.number = number
self.country_code = country_code
class WhatsAppAccount:
def __init__(self, number, country_code):
self.number = number
self.country_code = country_code
class TestEnvironment:
def __init__(self):
self.sms_test_account = SmsAccount("0000000000", "ZA")
self.email_test_account = "test-notifications@yourdomain.com"
self.whats_app_test_account = WhatsAppAccount("0000000000", "ZA")
# Static class equivalent in Python (using module-level variable)
class TestAccounts:
Accounts = TestEnvironment()
class SmsAccount {
constructor(number, countryCode) {
this.number = number;
this.countryCode = countryCode;
}
}
class WhatsAppAccount {
constructor(number, countryCode) {
this.number = number;
this.countryCode = countryCode;
}
}
class TestEnvironment {
constructor() {
this.smsTestAccount = new SmsAccount("0000000000", "ZA");
this.emailTestAccount = "test-notifications@yourdomain.com";
this.whatsAppTestAccount = new WhatsAppAccount("0000000000", "ZA");
}
}
// Static equivalent in JavaScript
const TestAccounts = {
Accounts: new TestEnvironment()
};
Performance Optimization
Optimize notification processing for high-volume scenarios:
- Batch Processing: Group notifications when sending to multiple recipients
// Batch SMS example
POST /Sms/SendBatch
{
"body": "Your account has been updated",
"sendTo": [
{ "address": "0123456789", "countryCode": "ZA" },
{ "address": "0123456790", "countryCode": "ZA" },
{ "address": "0123456791", "countryCode": "ZA" }
]
}
- Asynchronous Processing: Queue notifications for background processing
- C#
- Java
- Python
- JavaScript
public class NotificationQueue
{
private readonly IServiceScopeFactory _scopeFactory;
public NotificationQueue(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task QueueNotificationAsync(Notification notification)
{
// Add to queue
await _notificationQueue.EnqueueAsync(notification);
// Process in background
_ = Task.Run(async () => await ProcessQueueAsync());
}
private async Task ProcessQueueAsync()
{
using var scope = _scopeFactory.CreateScope();
var sender = scope.ServiceProvider.GetRequiredService<INotificationSender>();
while (await _notificationQueue.TryDequeueAsync(out var notification))
{
try
{
await sender.SendAsync(notification);
}
catch (Exception ex)
{
// Log exception and potentially requeue with backoff
}
}
}
}
public class NotificationQueue {
private final BlockingQueue<Notification> notificationQueue = new LinkedBlockingQueue<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final Provider<NotificationSender> senderProvider;
@Inject
public NotificationQueue(Provider<NotificationSender> senderProvider) {
this.senderProvider = senderProvider;
// Start the queue processor
executor.submit(this::processQueue);
}
public CompletableFuture<Void> queueNotificationAsync(Notification notification) {
return CompletableFuture.runAsync(() -> {
try {
notificationQueue.put(notification);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Failed to queue notification", e);
}
});
}
private void processQueue() {
NotificationSender sender = senderProvider.get();
while (!Thread.currentThread().isInterrupted()) {
try {
Notification notification = notificationQueue.take();
try {
sender.sendAsync(notification).get();
} catch (Exception ex) {
// Log exception and potentially requeue with backoff
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
import asyncio
from queue import Queue
from threading import Thread
class NotificationQueue:
def __init__(self, notification_sender_factory):
self._notification_queue = asyncio.Queue()
self._notification_sender_factory = notification_sender_factory
# Start queue processing
asyncio.create_task(self._process_queue())
async def queue_notification_async(self, notification):
# Add to queue
await self._notification_queue.put(notification)
async def _process_queue(self):
sender = self._notification_sender_factory()
while True:
try:
notification = await self._notification_queue.get()
try:
await sender.send_async(notification)
except Exception as ex:
# Log exception and potentially requeue with backoff
pass
finally:
self._notification_queue.task_done()
except Exception as ex:
# Handle unexpected errors in queue processing
pass
class NotificationQueue {
constructor(notificationSenderFactory) {
this._queue = [];
this._processing = false;
this._notificationSenderFactory = notificationSenderFactory;
}
async queueNotificationAsync(notification) {
// Add to queue
this._queue.push(notification);
// Start processing if not already running
if (!this._processing) {
this._processQueue();
}
}
async _processQueue() {
this._processing = true;
const sender = this._notificationSenderFactory();
while (this._queue.length > 0) {
const notification = this._queue.shift();
try {
await sender.sendAsync(notification);
} catch (ex) {
// Log exception and potentially requeue with backoff
}
}
this._processing = false;
}
}
- Priority Queues: Implement priority-based sending for time-sensitive alerts
- Caching: Cache template data and user preferences to reduce database load
Analytics and Improvement
Track notification effectiveness to continuously improve your strategy:
- Delivery Rates: Monitor successful deliveries across channels
- Open/Click Rates: Track engagement with notification content
- Conversion Tracking: Measure actions taken after notification receipt
- A/B Testing: Test different message formats and timing to optimize engagement
Compliance Requirements
Adhere to relevant regulations when sending notifications:
-
POPIA (South Africa)
- Obtain explicit consent before sending marketing messages
- Provide clear opt-out mechanisms in every message
- Maintain records of consent
-
GDPR (If dealing with EU users)
- Document lawful basis for processing before sending notifications
- Honor opt-out requests promptly
- Limit data retention periods
-
Industry-specific Requirements
- Financial services notifications may have additional regulatory requirements
- Healthcare notifications must comply with relevant privacy regulations
Next Steps
- Explore the API reference