# Notification System

Below is a comprehensive low-level design (LLD) for a Notification System in Java. In this design we use:

* **Singleton Pattern:** To ensure a single instance of the NotificationManager.
* **Factory Pattern:** To centralize the creation of Notification objects.
* **Strategy Pattern:** To choose the appropriate delivery mechanism (Email, SMS, Push) at runtime.
* **Observer Pattern:** To decouple notification events from additional processing (for example, logging or other side effects).

We also address key requirements:

* **Modeling Notifications and Users:** Representing users with various contact details and notifications with type and content.
* **Delivery Mechanism:** Using different strategies to deliver notifications via email, SMS, or push.
* **Reliable Delivery:** Leveraging asynchronous execution (using ExecutorService) and potential retry mechanisms.

***

### 1. Overview

* **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

```java
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.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://mayanktyagi3111.gitbook.io/interview-prep/lld-questions/notification-system.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
