User: Represents a recipient with contact details (email, phone, device token).
Notification: Encapsulates a message with content and type (EMAIL, SMS, PUSH).
NotificationManager: Centralizes sending notifications reliably using asynchronous delivery and selects the proper delivery strategy.
NotificationFactory: Creates notifications in a standardized manner.
Delivery Strategies: Different implementations (Email, SMS, Push) encapsulate the logic of sending a notification.
Observer: Allows additional components (like a logger) to observe notification events without coupling to the NotificationManager.
2. Detailed Java Code
import java.util.*;
import java.util.concurrent.*;
// --------------------- ENTITIES ---------------------
// User entity representing a recipient of notifications.
class User {
private String id;
private String name;
private String email;
private String phone;
private String deviceToken; // For push notifications
public User(String id, String name, String email, String phone, String deviceToken) {
this.id = id;
this.name = name;
this.email = email;
this.phone = phone;
this.deviceToken = deviceToken;
}
// Getters for user contact details.
public String getEmail() { return email; }
public String getPhone() { return phone; }
public String getDeviceToken() { return deviceToken; }
public String getName() { return name; }
}
// Notification entity representing a message to be delivered.
class Notification {
private String notificationId;
private String content;
private NotificationType type;
private boolean delivered;
public Notification(String content, NotificationType type) {
// Factory Pattern creates these objects uniformly.
this.notificationId = UUID.randomUUID().toString();
this.content = content;
this.type = type;
this.delivered = false;
}
public String getContent() { return content; }
public NotificationType getType() { return type; }
public void markAsDelivered() { delivered = true; }
public boolean isDelivered() { return delivered; }
}
// Enum representing notification types.
enum NotificationType {
EMAIL, SMS, PUSH
}
// --------------------- FACTORY ---------------------
// Factory Pattern: Centralizes creation of Notification objects.
class NotificationFactory {
public static Notification createNotification(String content, NotificationType type) {
return new Notification(content, type);
}
}
// --------------------- STRATEGY INTERFACE ---------------------
// Strategy Pattern: Defines a contract for delivery mechanisms.
interface DeliveryStrategy {
// Deliver the notification to the given user.
void deliver(Notification notification, User user);
}
// Concrete strategy for Email delivery.
class EmailDeliveryStrategy implements DeliveryStrategy {
@Override
public void deliver(Notification notification, User user) {
// Simulate sending an email.
System.out.println("Sending EMAIL to " + user.getEmail() + ": " + notification.getContent());
// Here you would integrate with an email service API.
}
}
// Concrete strategy for SMS delivery.
class SMSDeliveryStrategy implements DeliveryStrategy {
@Override
public void deliver(Notification notification, User user) {
// Simulate sending an SMS.
System.out.println("Sending SMS to " + user.getPhone() + ": " + notification.getContent());
// Here you would integrate with an SMS gateway.
}
}
// Concrete strategy for Push notification delivery.
class PushDeliveryStrategy implements DeliveryStrategy {
@Override
public void deliver(Notification notification, User user) {
// Simulate sending a push notification.
System.out.println("Sending PUSH to device " + user.getDeviceToken() + ": " + notification.getContent());
// Here you would integrate with a push notification service.
}
}
// --------------------- OBSERVER INTERFACE ---------------------
// Observer Pattern: Allows decoupled components to react to notification events.
interface NotificationObserver {
void update(Notification notification, User user);
}
// Concrete observer that logs notifications.
class LoggingObserver implements NotificationObserver {
@Override
public void update(Notification notification, User user) {
System.out.println("Logging: Notification [" + notification.getNotificationId() + "] sent to " + user.getName());
}
}
// --------------------- NOTIFICATION MANAGER ---------------------
// Singleton Pattern: Ensures a single instance of NotificationManager.
// Also uses the Strategy Pattern to choose the proper delivery mechanism.
// Additionally, incorporates the Observer Pattern to notify registered observers.
class NotificationManager {
private static NotificationManager instance;
// ExecutorService for asynchronous and reliable delivery.
private ExecutorService executor = Executors.newFixedThreadPool(5);
// Map associating notification types with their delivery strategies.
private Map<NotificationType, DeliveryStrategy> strategyMap;
// List of observers for notification events.
private List<NotificationObserver> observers;
// Private constructor (Singleton Pattern).
private NotificationManager() {
strategyMap = new HashMap<>();
observers = new ArrayList<>();
// Configure strategies.
strategyMap.put(NotificationType.EMAIL, new EmailDeliveryStrategy());
strategyMap.put(NotificationType.SMS, new SMSDeliveryStrategy());
strategyMap.put(NotificationType.PUSH, new PushDeliveryStrategy());
}
// Public method to retrieve the singleton instance.
public static synchronized NotificationManager getInstance() {
if (instance == null) {
instance = new NotificationManager();
}
return instance;
}
// Method to register an observer (Observer Pattern).
public void registerObserver(NotificationObserver observer) {
observers.add(observer);
}
// Method to notify all observers about a delivered notification.
private void notifyObservers(Notification notification, User user) {
for (NotificationObserver observer : observers) {
observer.update(notification, user);
}
}
// Reliable, asynchronous delivery of notifications.
public void sendNotification(final Notification notification, final User user) {
// Strategy Pattern: Select the delivery mechanism based on notification type.
DeliveryStrategy strategy = strategyMap.get(notification.getType());
if (strategy == null) {
System.out.println("No delivery strategy found for type: " + notification.getType());
return;
}
// Submit delivery task asynchronously.
executor.submit(() -> {
try {
// Attempt to deliver the notification.
strategy.deliver(notification, user);
notification.markAsDelivered();
// After delivery, notify observers (Observer Pattern).
notifyObservers(notification, user);
} catch (Exception e) {
// In case of delivery failure, log and potentially retry.
System.out.println("Delivery failed for " + user.getName() + ": " + e.getMessage());
// Retry logic can be implemented here.
}
});
}
// Shutdown the executor service gracefully.
public void shutdown() {
executor.shutdown();
}
}
// --------------------- MAIN APPLICATION ---------------------
public class NotificationSystemApp {
public static void main(String[] args) {
// Create sample users.
User alice = new User(UUID.randomUUID().toString(), "Alice", "alice@example.com", "1234567890", "device_token_alice");
User bob = new User(UUID.randomUUID().toString(), "Bob", "bob@example.com", "0987654321", "device_token_bob");
// Create notifications using the Factory Pattern.
Notification emailNotification = NotificationFactory.createNotification("Welcome to our service, Alice!", NotificationType.EMAIL);
Notification smsNotification = NotificationFactory.createNotification("Your verification code is 1234", NotificationType.SMS);
Notification pushNotification = NotificationFactory.createNotification("You have a new friend request", NotificationType.PUSH);
// Get the NotificationManager singleton instance.
NotificationManager manager = NotificationManager.getInstance();
// Register a LoggingObserver to observe notification events (Observer Pattern).
manager.registerObserver(new LoggingObserver());
// Send notifications reliably using asynchronous execution.
manager.sendNotification(emailNotification, alice);
manager.sendNotification(smsNotification, bob);
manager.sendNotification(pushNotification, alice);
// Allow some time for asynchronous tasks to complete.
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// Shutdown the NotificationManager's executor.
manager.shutdown();
}
}
3. Explanation of Key Points & Design Patterns
Modeling Notifications and Users
User Class:
Contains contact details (email, phone, device token) needed for various delivery channels.
Notification Class:
Encapsulates notification content and type, with a unique ID and delivery status.
Delivery Mechanism
DeliveryStrategy Interface (Strategy Pattern):
Defines a common method to deliver notifications.
Concrete Strategies:EmailDeliveryStrategy, SMSDeliveryStrategy, and PushDeliveryStrategy implement the delivery logic for each channel.
Reliable Delivery
NotificationManager (Singleton Pattern):
Manages sending notifications reliably using an ExecutorService for asynchronous processing.
Retry Logic (Extension Point):
In the delivery task, exceptions can be caught to implement retry mechanisms if needed.
Additional Patterns
Factory Pattern:NotificationFactory is used to create Notification objects uniformly, ensuring consistent instantiation.
Observer Pattern:NotificationManager maintains a list of observers (like LoggingObserver) that are notified after a successful delivery. This decouples side-effect operations (such as logging) from the main delivery logic.
4. Conclusion
This LLD for a Notification System demonstrates how to:
Model users and notifications with clear entity classes.
Handle multiple delivery mechanisms using the Strategy Pattern.
Ensure reliable and asynchronous delivery using the Singleton-managed NotificationManager and ExecutorService.
Incorporate additional behaviors (e.g., logging) using the Observer Pattern.
Centralize object creation using the Factory Pattern.
This design is modular, extensible, and would serve as a strong discussion point in an interview setting.