Definition
- Notification system : accepts notification requests from many producers, selects delivery channels, renders content, and reliably delivers through external providers.
- Async decoupling : producers publish events to a queue and return immediately; consumers handle slow external delivery.
- Template engine : fills placeholders in message templates with event data for consistent, translatable content.
- Priority lanes : separate queues or priority levels so critical alerts (fraud) are never delayed by bulk marketing.
- Dead-letter queue : captures messages that fail after max retries for investigation.
When Rajesh transfers money at National Bank of India, he should get a confirmation. But the bank does not make him wait while the SMS gateway responds. Instead the transfer service drops a note in an outbox, and a dedicated notifications team picks it up, checks how Rajesh prefers to be contacted, writes the message from a template, and sends it through the right channel, retrying if the provider is down.
Understanding the Concepts
Why is async decoupling essential?
A producer like the transfer service publishes a notification event to a queue and returns immediately. Consumers do the slow work of calling external providers. A slow SMS gateway never slows a money transfer.
package com.bank.notification;
public class TransferNotificationPublisher {
public void onTransferCompleted(Transfer t) {
notificationQueue.publish(new NotificationEvent(
t.accountId(), NotificationType.TRANSFER_SUCCESS,
Map.of("amount", t.amount().toString(), "to", t.toAccount()),
Priority.HIGH));
}
}
How does channel selection work?
Each user record stores which channels they opted into. The system respects do-not-disturb windows and regulatory opt-outs.
package com.bank.notification;
public class ChannelSelector {
public List<Channel> select(String userId, NotificationType type) {
UserPreferences prefs = prefStore.get(userId);
if (prefs.isDoNotDisturb()) return List.of();
return prefs.channelsFor(type); // e.g., [PUSH, SMS]
}
}
How does template rendering work?
A template engine fills placeholders with event data, keeping messages consistent and translatable.
package com.bank.notification;
public class TemplateRenderer {
public String render(String templateId, Map<String, String> data) {
String template = templateStore.get(templateId);
// "Dear {name}, Rs.{amount} transferred to {to}"
for (var entry : data.entrySet())
template = template.replace("{" + entry.getKey() + "}", entry.getValue());
return template;
}
}
How does priority handling work?
Critical messages like fraud alerts use a high-priority queue that is consumed first. Marketing messages use a low-priority queue that yields to security alerts.
How does retry logic work?
Retry transient failures with exponential backoff. After max attempts, move to a dead-letter queue for investigation.
package com.bank.notification;
public class RetryHandler {
public void handleWithRetry(NotificationEvent event) {
if (event.attempts() >= MAX_RETRIES) {
deadLetterQueue.send(event);
return;
}
try { deliver(event); }
catch (Exception e) {
retryQueue.sendWithDelay(event.incrementAttempts(),
backoff(event.attempts()));
}
}
}
How do you ensure idempotent delivery?
Each notification carries a stable id. The dispatcher records delivered ids, so a redelivery is recognized and the customer is never texted twice.
How do you track delivery status?
Track at each stage: queued, sent, delivered, failed. This gives operations visibility into notification health.
What should a senior engineer know?
A senior engineer designs for provider failover (primary SMS provider down, fall back to secondary), monitors delivery rates and latency by channel, and ensures notifications comply with regulatory requirements like opt-out handling.
Visual Overview
Notification System Architecture
┌──────────┐ event ┌──────────┐ ┌──────────────────┐
│ Transfer │────────►│ Kafka │ │ Priority Lanes: │
│ Service │ │ Queue │ │ HIGH: fraud/txn │
└──────────┘ └────┬─────┘ │ LOW: marketing │
┌──────────┐ │ └──────────────────┘
│ Fraud │──── event ───┘
│ Service │ │
└──────────┘ ▼
┌───────────────┐
│ Dispatcher │
│ 1. Prefs │
│ 2. Template │
│ 3. Channel │
└──┬────┬────┬──┘
│ │ │
┌────▼┐ ┌─▼──┐ ┌▼────┐
│Push │ │SMS │ │Email│
│APNs │ │Gate│ │SMTP │
│FCM │ │way │ │ │
└─────┘ └────┘ └─────┘
│
fail after max retries
▼
┌──────────┐
│ Dead │
│ Letter Q │
└──────────┘
Key Takeaway
A notification system accepts requests from many producers, chooses delivery channels from user preferences, renders content from templates, and reliably delivers through external providers. The single most important design choice is async decoupling: producers publish events to a queue and return immediately, so a slow provider never holds a money transfer hostage.
I use priority lanes so fraud alerts always jump ahead of marketing blasts, retry transient failures with exponential backoff, and move persistent failures to a dead-letter queue. Every notification carries a stable id for idempotent delivery so a redelivery never texts the customer twice.
At National Bank of India, this ensured transfer confirmations reached Rajesh reliably without slowing down the transfer itself, and fraud alerts were never delayed by a bulk SMS campaign.
Remember one sentence: decouple notification delivery from business logic with a queue, select channels from user preferences, and make delivery idempotent so no customer is notified twice.
---