Skip to main content

Notification System

A service that sends the same alert over email, SMS, or push without the caller ever knowing which one actually fired. The interesting decision isn't "how do I send an SMS" - it's how to keep NotificationService ignorant of the channel it's using.

Requirements

Functional

  • A caller sends a notification to a user through one or more channels (email, SMS, push).
  • Each channel has its own delivery mechanism and its own way of formatting a message.
  • A user can opt out of a channel; the service must not deliver through a channel the user has disabled.
  • Failed sends should be retried a bounded number of times before being given up on.

Non-functional

  • Adding a new channel (say, WhatsApp) must not require editing NotificationService or any existing channel class.
  • Sending must not block the caller on slow network channels for longer than necessary (this page models the retry loop; a real system would hand delivery to a queue).

Design

NotificationService takes a message and a list of NotificationChannel implementations and fires each one - it never has an if (channel == EMAIL) anywhere. Each channel owns its own formatting and its own transport, so a new channel is a new class, not a new branch.

CallerNotificationServiceNotificationPreferencesNotificationChannelnotify(user, message)1enabledChannels(user)2sendWithRetry(message)3format(message)4retry on failure5
  1. 1The caller hands over a user and a message - nothing about how it should be delivered.
  2. 2The service asks preferences which channels this user actually wants, not which channels exist.
  3. 3For each enabled channel the service calls the same method, regardless of channel type.
  4. 4Each channel formats the message its own way - SMS truncates, email adds a subject line.
  5. 5Retry policy lives in the service so it applies uniformly, not per channel.

Retrying lives in the service, not the channel, because "how many times to retry" is a policy decision that should be the same regardless of which channel is flaky today.

Class diagram

«interface»NotificationChannel+ send(user, message): bool+ type(): ChannelTypeNotificationService- channels: List<NotificationChannel>- prefs: NotificationPreferences- maxRetries: int+ notify(user, message): voidNotificationPreferences- optedOut: Map<User, Set<ChannelType>>+ isEnabled(user, type): boolMessage- subject: string- body: stringEmailChannel+ send(user, message): boolSmsChannel+ send(user, message): boolPushChannel+ send(user, message): bool
implementsuses
NotificationService fans a message out to every enabled NotificationChannel; each channel formats and sends on its own terms.

Code

import java.util.*;
 
enum ChannelType { EMAIL, SMS, PUSH }
 
class User {
final String id;
final String email;
final String phone;
 
User(String id, String email, String phone) {
this.id = id;
this.email = email;
this.phone = phone;
}
}
 
class Message {
final String subject;
final String body;
 
Message(String subject, String body) {
this.subject = subject;
this.body = body;
}
}
 
interface NotificationChannel {
ChannelType type();
boolean send(User user, Message message);
}
 
class EmailChannel implements NotificationChannel {
public ChannelType type() { return ChannelType.EMAIL; }
 
public boolean send(User user, Message message) {
String formatted = "Subject: " + message.subject + "\n" + message.body;
System.out.println("Emailing " + user.email + ": " + formatted);
return true;
}
}
 
class SmsChannel implements NotificationChannel {
public ChannelType type() { return ChannelType.SMS; }
 
public boolean send(User user, Message message) {
String truncated = message.body.length() > 140
? message.body.substring(0, 140)
: message.body;
System.out.println("Texting " + user.phone + ": " + truncated);
return true;
}
}
 
class PushChannel implements NotificationChannel {
public ChannelType type() { return ChannelType.PUSH; }
 
public boolean send(User user, Message message) {
System.out.println("Pushing to " + user.id + ": " + message.subject);
return true;
}
}
 
class NotificationPreferences {
private final Map<String, Set<ChannelType>> optedOut = new HashMap<>();
 
void optOut(User user, ChannelType type) {
optedOut.computeIfAbsent(user.id, k -> new HashSet<>()).add(type);
}
 
boolean isEnabled(User user, ChannelType type) {
return !optedOut.getOrDefault(user.id, Set.of()).contains(type);
}
}
 
class NotificationService {
private final List<NotificationChannel> channels;
private final NotificationPreferences prefs;
private final int maxRetries;
 
NotificationService(List<NotificationChannel> channels, NotificationPreferences prefs, int maxRetries) {
this.channels = channels;
this.prefs = prefs;
this.maxRetries = maxRetries;
}
 
void notify(User user, Message message) {
for (NotificationChannel channel : channels) {
if (!prefs.isEnabled(user, channel.type())) continue;
sendWithRetry(channel, user, message);
}
}
 
private void sendWithRetry(NotificationChannel channel, User user, Message message) {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
if (channel.send(user, message)) return;
}
System.out.println("Gave up on " + channel.type() + " for " + user.id);
}
}

Design decisions

  • NotificationChannel is an interface, not a switch statement. The alternative - one method with a branch per channel type - means every new channel touches a shared file and risks breaking an unrelated one. An interface makes each channel additive: drop in a class, register it, done.
  • Opt-out lives on NotificationPreferences, not baked into each channel. A channel only knows how to send; whether it should send for a given user is a separate question answered once, in one place, instead of every channel re-implementing the same check.
  • Retry is a wrapper around send, not duplicated in every channel. NotificationService calls sendWithRetry, which calls the channel's send up to a fixed number of times. Every channel gets retry behavior for free and none of them had to write it themselves.
  • What's missing for a real system: delivery here is synchronous and in-process; a production version would enqueue each channel send onto a message queue so a slow SMS provider can't hold up an email that would have gone through instantly, and would track delivery receipts per channel rather than a single boolean.
0%0 of 122 pages studied