Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleMicrosoftFlipkartSwiggyRazorpayCRED

Observer Pattern - Event System, Publish-Subscribe

Choose your preparation mode3 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
🎯
Observer Pattern - Event System, Publish-Subscribe
mediumOOPAmazonGoogleMicrosoft

Imagine a news app where users want to receive updates only on topics they care about. How can the app notify all interested users automatically when a new article is published?

💡 This problem is about designing a system where multiple objects (observers) need to be notified automatically when another object (subject) changes state. Beginners often struggle because they try to tightly couple components or miss the dynamic subscription aspect, which leads to rigid and hard-to-maintain code.
📋
Problem Statement

Design and implement an event notification system using the Observer pattern. The system should allow multiple observers (listeners) to subscribe to a subject (publisher). When the subject's state changes or an event occurs, all subscribed observers should be notified automatically. Implement methods to add and remove observers, and to notify them of events.

Number of observers can be large (up to 10^5)Observers can subscribe or unsubscribe at any timeNotifications should be sent in the order observers subscribedThe system should be thread-safe if implemented in a concurrent environment (optional)
💡
Example
Input"Subject created; Observer A and B subscribe; Subject state changes"
OutputObserver A notified; Observer B notified

Both observers receive notification in subscription order when the subject changes state

  • No observers subscribed → no notifications sent
  • Observer unsubscribes before notification → observer not notified
  • Multiple observers subscribe and unsubscribe repeatedly → system remains consistent
  • Subject notifies with no state change → observers still notified if event triggers
⚠️
Common Mistakes
Not removing observers properly

Unsubscribed observers still receive notifications causing unexpected behavior

Implement unsubscribe method that correctly removes observers from all relevant collections

Modifying observer list during notification

Runtime errors or skipped notifications due to concurrent modification

Use a snapshot copy of observers before notifying or use concurrent-safe collections

Tight coupling between subject and observers

Hard to extend or reuse code; violates design principles

Use interfaces or abstract classes for observers and subjects to decouple implementations

Not handling event filtering

All observers get all notifications, causing inefficiency and irrelevant updates

Implement event-type based subscription to notify only interested observers

Ignoring thread safety in concurrent environments

Race conditions, crashes, or inconsistent notifications

Use locks or concurrent data structures to synchronize access

🧠
Brute Force (Simple List with Manual Notification)
💡 Starting with a simple list of observers helps understand the core mechanism of subscription and notification without worrying about performance or concurrency.

Intuition

Maintain a list of observers and iterate over it to notify each observer when an event occurs. Adding and removing observers is done by appending or removing from the list.

Algorithm

  1. Create a Subject class that holds a list of observers.
  2. Implement methods to add and remove observers from the list.
  3. When an event occurs, iterate over the list and call the notify method on each observer.
  4. Observers implement a common interface with an update method to receive notifications.
💡 The challenge is to keep track of observers and ensure all get notified. This approach is easy to visualize but can be slow if the list is large or if removals are frequent.
</>
Code
class Observer:
    def update(self, message):
        pass

class Subject:
    def __init__(self):
        self.observers = []

    def subscribe(self, observer):
        self.observers.append(observer)

    def unsubscribe(self, observer):
        if observer in self.observers:
            self.observers.remove(observer)

    def notify(self, message):
        for observer in self.observers:
            observer.update(message)

# Example usage
class ConcreteObserver(Observer):
    def __init__(self, name):
        self.name = name

    def update(self, message):
        print(f"{self.name} received: {message}")

subject = Subject()
obs1 = ConcreteObserver("Observer A")
obs2 = ConcreteObserver("Observer B")
subject.subscribe(obs1)
subject.subscribe(obs2)
subject.notify("Event 1 occurred")
subject.unsubscribe(obs1)
subject.notify("Event 2 occurred")
Line Notes
self.observers = []Initialize an empty list to hold all subscribed observers to maintain order
self.observers.append(observer)Add a new observer to the subscription list to receive future notifications
if observer in self.observers:Check if observer is currently subscribed before removing to avoid errors
for observer in self.observers:Notify each observer by calling their update method sequentially
import java.util.ArrayList;
import java.util.List;

interface Observer {
    void update(String message);
}

class Subject {
    private List<Observer> observers = new ArrayList<>();

    public void subscribe(Observer observer) {
        observers.add(observer);
    }

    public void unsubscribe(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    public void update(String message) {
        System.out.println(name + " received: " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Subject subject = new Subject();
        ConcreteObserver obs1 = new ConcreteObserver("Observer A");
        ConcreteObserver obs2 = new ConcreteObserver("Observer B");
        subject.subscribe(obs1);
        subject.subscribe(obs2);
        subject.notifyObservers("Event 1 occurred");
        subject.unsubscribe(obs1);
        subject.notifyObservers("Event 2 occurred");
    }
}
Line Notes
private List<Observer> observers = new ArrayList<>();Store observers in a dynamic list to maintain subscription order
observers.add(observer);Subscribe an observer by adding to the list for future notifications
observers.remove(observer);Unsubscribe by removing the observer if present to stop notifications
for (Observer observer : observers)Notify all current observers sequentially by calling update
#include <iostream>
#include <vector>
#include <algorithm>

class Observer {
public:
    virtual void update(const std::string& message) = 0;
    virtual ~Observer() {}
};

class Subject {
private:
    std::vector<Observer*> observers;
public:
    void subscribe(Observer* observer) {
        observers.push_back(observer);
    }

    void unsubscribe(Observer* observer) {
        observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
    }

    void notify(const std::string& message) {
        for (Observer* observer : observers) {
            observer->update(message);
        }
    }
};

class ConcreteObserver : public Observer {
private:
    std::string name;
public:
    ConcreteObserver(const std::string& n) : name(n) {}
    void update(const std::string& message) override {
        std::cout << name << " received: " << message << std::endl;
    }
};

int main() {
    Subject subject;
    ConcreteObserver obs1("Observer A");
    ConcreteObserver obs2("Observer B");
    subject.subscribe(&obs1);
    subject.subscribe(&obs2);
    subject.notify("Event 1 occurred");
    subject.unsubscribe(&obs1);
    subject.notify("Event 2 occurred");
    return 0;
}
Line Notes
std::vector<Observer*> observers;Use a vector to hold pointers to observers to maintain order and allow dynamic management
observers.push_back(observer);Add observer pointer to the list for future notifications
observers.erase(std::remove(...))Remove observer pointer safely from the vector to stop notifications
for (Observer* observer : observers)Iterate over all observers to notify them sequentially
class Observer {
    update(message) {}
}

class Subject {
    constructor() {
        this.observers = [];
    }

    subscribe(observer) {
        this.observers.push(observer);
    }

    unsubscribe(observer) {
        this.observers = this.observers.filter(obs => obs !== observer);
    }

    notify(message) {
        this.observers.forEach(observer => observer.update(message));
    }
}

class ConcreteObserver extends Observer {
    constructor(name) {
        super();
        this.name = name;
    }

    update(message) {
        console.log(`${this.name} received: ${message}`);
    }
}

// Example usage
const subject = new Subject();
const obs1 = new ConcreteObserver("Observer A");
const obs2 = new ConcreteObserver("Observer B");
subject.subscribe(obs1);
subject.subscribe(obs2);
subject.notify("Event 1 occurred");
subject.unsubscribe(obs1);
subject.notify("Event 2 occurred");
Line Notes
this.observers = []Initialize an array to hold subscribed observers maintaining order
this.observers.push(observer)Add observer to the subscription list for notifications
this.observers = this.observers.filter(...)Remove observer by filtering out from the list to unsubscribe
this.observers.forEach(observer => observer.update(message))Notify all observers by calling their update method sequentially
Complexity
TimeO(n) per notification where n is number of observers
SpaceO(n) for storing observers

Each notify call iterates over all observers, so time grows linearly with number of observers. Space is linear due to storing all observers.

💡 If there are 1000 observers, notifying all means 1000 calls to update, which can be slow if frequent.
Interview Verdict: Accepted - good for understanding but not optimal for large-scale or concurrent systems

This approach is simple and clear, perfect for interviews to explain the pattern, but can be inefficient if observers are many or frequent changes happen.

🧠
Improved Approach with HashSet and Event Filtering
💡 Using a hash set for observers improves removal efficiency. Adding event filtering allows notifying only interested observers, making the system more scalable and flexible.

Intuition

Store observers in a hash set for O(1) add/remove. Each observer registers interest in specific event types. Notify only observers interested in the event type.

Algorithm

  1. Use a hash set or dictionary to store observers for O(1) add/remove.
  2. Allow observers to specify event types they want to receive.
  3. Maintain a mapping from event types to sets of observers.
  4. When notifying, only iterate over observers subscribed to that event type.
💡 This adds complexity but improves efficiency and flexibility by avoiding unnecessary notifications.
</>
Code
class Observer:
    def __init__(self):
        self.events = set()
    def update(self, event_type, message):
        pass

class Subject:
    def __init__(self):
        self.observers = {}

    def subscribe(self, observer, event_type):
        if event_type not in self.observers:
            self.observers[event_type] = set()
        self.observers[event_type].add(observer)

    def unsubscribe(self, observer, event_type):
        if event_type in self.observers and observer in self.observers[event_type]:
            self.observers[event_type].remove(observer)
            if not self.observers[event_type]:
                del self.observers[event_type]

    def notify(self, event_type, message):
        if event_type in self.observers:
            for observer in self.observers[event_type]:
                observer.update(event_type, message)

class ConcreteObserver(Observer):
    def __init__(self, name):
        super().__init__()
        self.name = name

    def update(self, event_type, message):
        print(f"{self.name} received {event_type}: {message}")

# Example usage
subject = Subject()
obs1 = ConcreteObserver("Observer A")
obs2 = ConcreteObserver("Observer B")
subject.subscribe(obs1, "news")
subject.subscribe(obs2, "sports")
subject.notify("news", "News event 1")
subject.notify("sports", "Sports event 1")
subject.unsubscribe(obs1, "news")
subject.notify("news", "News event 2")
Line Notes
self.observers = {}Dictionary maps event types to sets of observers for quick lookup and efficient filtering
self.observers[event_type].add(observer)Add observer to the set for the specific event type to receive relevant notifications
if event_type in self.observers:Notify only observers subscribed to this event type to avoid unnecessary calls
for observer in self.observers[event_type]:Iterate over relevant observers to notify them about the event
import java.util.*;

interface Observer {
    void update(String eventType, String message);
}

class Subject {
    private Map<String, Set<Observer>> observers = new HashMap<>();

    public void subscribe(Observer observer, String eventType) {
        observers.computeIfAbsent(eventType, k -> new HashSet<>()).add(observer);
    }

    public void unsubscribe(Observer observer, String eventType) {
        if (observers.containsKey(eventType)) {
            Set<Observer> set = observers.get(eventType);
            set.remove(observer);
            if (set.isEmpty()) {
                observers.remove(eventType);
            }
        }
    }

    public void notifyObservers(String eventType, String message) {
        if (observers.containsKey(eventType)) {
            for (Observer observer : observers.get(eventType)) {
                observer.update(eventType, message);
            }
        }
    }
}

class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    public void update(String eventType, String message) {
        System.out.println(name + " received " + eventType + ": " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Subject subject = new Subject();
        ConcreteObserver obs1 = new ConcreteObserver("Observer A");
        ConcreteObserver obs2 = new ConcreteObserver("Observer B");
        subject.subscribe(obs1, "news");
        subject.subscribe(obs2, "sports");
        subject.notifyObservers("news", "News event 1");
        subject.notifyObservers("sports", "Sports event 1");
        subject.unsubscribe(obs1, "news");
        subject.notifyObservers("news", "News event 2");
    }
}
Line Notes
private Map<String, Set<Observer>> observers = new HashMap<>();Map event types to sets of observers for efficient event filtering and quick access
observers.computeIfAbsent(eventType, k -> new HashSet<>()).add(observer);Add observer to the set for the event type, creating set if missing to maintain subscriptions
if (observers.containsKey(eventType))Check if any observers subscribed to this event type before notifying to avoid null errors
for (Observer observer : observers.get(eventType))Notify only observers interested in this event type to improve efficiency
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <string>

class Observer {
public:
    virtual void update(const std::string& eventType, const std::string& message) = 0;
    virtual ~Observer() {}
};

class Subject {
private:
    std::unordered_map<std::string, std::unordered_set<Observer*>> observers;
public:
    void subscribe(Observer* observer, const std::string& eventType) {
        observers[eventType].insert(observer);
    }

    void unsubscribe(Observer* observer, const std::string& eventType) {
        if (observers.count(eventType)) {
            observers[eventType].erase(observer);
            if (observers[eventType].empty()) {
                observers.erase(eventType);
            }
        }
    }

    void notify(const std::string& eventType, const std::string& message) {
        if (observers.count(eventType)) {
            for (auto observer : observers[eventType]) {
                observer->update(eventType, message);
            }
        }
    }
};

class ConcreteObserver : public Observer {
private:
    std::string name;
public:
    ConcreteObserver(const std::string& n) : name(n) {}
    void update(const std::string& eventType, const std::string& message) override {
        std::cout << name << " received " << eventType << ": " << message << std::endl;
    }
};

int main() {
    Subject subject;
    ConcreteObserver obs1("Observer A");
    ConcreteObserver obs2("Observer B");
    subject.subscribe(&obs1, "news");
    subject.subscribe(&obs2, "sports");
    subject.notify("news", "News event 1");
    subject.notify("sports", "Sports event 1");
    subject.unsubscribe(&obs1, "news");
    subject.notify("news", "News event 2");
    return 0;
}
Line Notes
std::unordered_map<std::string, std::unordered_set<Observer*>> observers;Map event types to sets of observer pointers for fast lookup and filtering
observers[eventType].insert(observer);Insert observer pointer into the set for the event type to subscribe
if (observers.count(eventType))Check if observers exist for the event type before notifying to avoid errors
for (auto observer : observers[eventType])Notify only observers subscribed to this event type to improve efficiency
class Observer {
    update(eventType, message) {}
}

class Subject {
    constructor() {
        this.observers = new Map();
    }

    subscribe(observer, eventType) {
        if (!this.observers.has(eventType)) {
            this.observers.set(eventType, new Set());
        }
        this.observers.get(eventType).add(observer);
    }

    unsubscribe(observer, eventType) {
        if (this.observers.has(eventType)) {
            this.observers.get(eventType).delete(observer);
            if (this.observers.get(eventType).size === 0) {
                this.observers.delete(eventType);
            }
        }
    }

    notify(eventType, message) {
        if (this.observers.has(eventType)) {
            this.observers.get(eventType).forEach(observer => observer.update(eventType, message));
        }
    }
}

class ConcreteObserver extends Observer {
    constructor(name) {
        super();
        this.name = name;
    }

    update(eventType, message) {
        console.log(`${this.name} received ${eventType}: ${message}`);
    }
}

// Example usage
const subject = new Subject();
const obs1 = new ConcreteObserver("Observer A");
const obs2 = new ConcreteObserver("Observer B");
subject.subscribe(obs1, "news");
subject.subscribe(obs2, "sports");
subject.notify("news", "News event 1");
subject.notify("sports", "Sports event 1");
subject.unsubscribe(obs1, "news");
subject.notify("news", "News event 2");
Line Notes
this.observers = new Map();Use a Map to associate event types with sets of observers for efficient filtering
this.observers.get(eventType).add(observer);Add observer to the set for the event type to subscribe
if (this.observers.has(eventType))Check if observers exist for event type before notifying to avoid errors
this.observers.get(eventType).forEach(observer => observer.update(...))Notify only observers subscribed to the event type to improve efficiency
Complexity
TimeO(k) per notification where k is number of observers subscribed to event type
SpaceO(n) for storing all observers across event types

Using sets and maps reduces notification to only relevant observers, improving efficiency over brute force. Add and remove operations are O(1) average due to hash sets.

💡 If 1000 observers exist but only 10 subscribe to 'news', notifying 'news' only calls 10 updates, saving time.
Interview Verdict: Accepted - better scalability and flexibility than brute force

This approach is more practical for real-world event systems where observers subscribe to specific events, improving performance and maintainability.

🧠
Thread-Safe Observer Pattern with Concurrent Collections
💡 In multi-threaded environments, observers may subscribe/unsubscribe while notifications happen. Using thread-safe collections prevents race conditions and inconsistent notifications.

Intuition

Use concurrent data structures (like ConcurrentHashMap and CopyOnWriteArrayList) to allow safe concurrent modifications and iteration during notification.

Algorithm

  1. Use thread-safe collections to store observers per event type.
  2. Allow concurrent subscribe/unsubscribe without locking the entire structure.
  3. Notify observers by iterating over a snapshot to avoid concurrent modification exceptions.
  4. Ensure notification order is preserved if required by using appropriate data structures.
💡 Concurrency adds complexity but is essential for robust real-world systems where multiple threads interact with the event system.
</>
Code
import threading

class Observer:
    def update(self, event_type, message):
        pass

class Subject:
    def __init__(self):
        self.lock = threading.Lock()
        self.observers = {}

    def subscribe(self, observer, event_type):
        with self.lock:
            if event_type not in self.observers:
                self.observers[event_type] = set()
            self.observers[event_type].add(observer)

    def unsubscribe(self, observer, event_type):
        with self.lock:
            if event_type in self.observers and observer in self.observers[event_type]:
                self.observers[event_type].remove(observer)
                if not self.observers[event_type]:
                    del self.observers[event_type]

    def notify(self, event_type, message):
        with self.lock:
            observers_snapshot = list(self.observers.get(event_type, []))
        for observer in observers_snapshot:
            observer.update(event_type, message)

class ConcreteObserver(Observer):
    def __init__(self, name):
        self.name = name

    def update(self, event_type, message):
        print(f"{self.name} received {event_type}: {message}")

# Example usage
subject = Subject()
obs1 = ConcreteObserver("Observer A")
obs2 = ConcreteObserver("Observer B")
subject.subscribe(obs1, "news")
subject.subscribe(obs2, "sports")
subject.notify("news", "News event 1")
subject.unsubscribe(obs1, "news")
subject.notify("news", "News event 2")
Line Notes
self.lock = threading.Lock()Create a lock to synchronize access to observers dictionary for thread safety
with self.lock:Ensure subscribe, unsubscribe, and notify snapshot are thread-safe by locking
observers_snapshot = list(self.observers.get(event_type, []))Copy observers to a snapshot to avoid holding lock during notification
for observer in observers_snapshot:Notify observers outside lock to prevent deadlocks and allow concurrent modifications
import java.util.*;
import java.util.concurrent.*;

interface Observer {
    void update(String eventType, String message);
}

class Subject {
    private final ConcurrentMap<String, CopyOnWriteArraySet<Observer>> observers = new ConcurrentHashMap<>();

    public void subscribe(Observer observer, String eventType) {
        observers.computeIfAbsent(eventType, k -> new CopyOnWriteArraySet<>()).add(observer);
    }

    public void unsubscribe(Observer observer, String eventType) {
        CopyOnWriteArraySet<Observer> set = observers.get(eventType);
        if (set != null) {
            set.remove(observer);
            if (set.isEmpty()) {
                observers.remove(eventType, set);
            }
        }
    }

    public void notifyObservers(String eventType, String message) {
        CopyOnWriteArraySet<Observer> set = observers.get(eventType);
        if (set != null) {
            for (Observer observer : set) {
                observer.update(eventType, message);
            }
        }
    }
}

class ConcreteObserver implements Observer {
    private String name;

    public ConcreteObserver(String name) {
        this.name = name;
    }

    public void update(String eventType, String message) {
        System.out.println(name + " received " + eventType + ": " + message);
    }
}

public class Main {
    public static void main(String[] args) {
        Subject subject = new Subject();
        ConcreteObserver obs1 = new ConcreteObserver("Observer A");
        ConcreteObserver obs2 = new ConcreteObserver("Observer B");
        subject.subscribe(obs1, "news");
        subject.subscribe(obs2, "sports");
        subject.notifyObservers("news", "News event 1");
        subject.unsubscribe(obs1, "news");
        subject.notifyObservers("news", "News event 2");
    }
}
Line Notes
ConcurrentMap<String, CopyOnWriteArraySet<Observer>> observersThread-safe map with thread-safe sets for concurrent access without explicit locking
observers.computeIfAbsent(eventType, k -> new CopyOnWriteArraySet<>()).add(observer);Add observer safely without locking entire map, allowing concurrent modifications
for (Observer observer : set)Iterate over a snapshot of observers safe from concurrent modification exceptions
observers.remove(eventType, set);Remove event type entry if no observers remain to keep map clean
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <string>
#include <mutex>
#include <vector>

class Observer {
public:
    virtual void update(const std::string& eventType, const std::string& message) = 0;
    virtual ~Observer() {}
};

class Subject {
private:
    std::unordered_map<std::string, std::unordered_set<Observer*>> observers;
    std::mutex mtx;
public:
    void subscribe(Observer* observer, const std::string& eventType) {
        std::lock_guard<std::mutex> lock(mtx);
        observers[eventType].insert(observer);
    }

    void unsubscribe(Observer* observer, const std::string& eventType) {
        std::lock_guard<std::mutex> lock(mtx);
        if (observers.count(eventType)) {
            observers[eventType].erase(observer);
            if (observers[eventType].empty()) {
                observers.erase(eventType);
            }
        }
    }

    void notify(const std::string& eventType, const std::string& message) {
        std::vector<Observer*> snapshot;
        {
            std::lock_guard<std::mutex> lock(mtx);
            if (observers.count(eventType)) {
                snapshot.assign(observers[eventType].begin(), observers[eventType].end());
            }
        }
        for (Observer* observer : snapshot) {
            observer->update(eventType, message);
        }
    }
};

class ConcreteObserver : public Observer {
private:
    std::string name;
public:
    ConcreteObserver(const std::string& n) : name(n) {}
    void update(const std::string& eventType, const std::string& message) override {
        std::cout << name << " received " << eventType << ": " << message << std::endl;
    }
};

int main() {
    Subject subject;
    ConcreteObserver obs1("Observer A");
    ConcreteObserver obs2("Observer B");
    subject.subscribe(&obs1, "news");
    subject.subscribe(&obs2, "sports");
    subject.notify("news", "News event 1");
    subject.unsubscribe(&obs1, "news");
    subject.notify("news", "News event 2");
    return 0;
}
Line Notes
std::mutex mtx;Mutex to synchronize access to observers map ensuring thread safety
std::lock_guard<std::mutex> lock(mtx);Lock mutex automatically for thread-safe subscribe/unsubscribe operations
snapshot.assign(observers[eventType].begin(), observers[eventType].end());Copy observers to a snapshot to release lock before notifying
for (Observer* observer : snapshot)Notify observers outside lock to avoid deadlocks and allow concurrent modifications
class Observer {
    update(eventType, message) {}
}

class Subject {
    constructor() {
        this.observers = new Map();
        this.lock = false; // simple lock simulation
    }

    subscribe(observer, eventType) {
        if (!this.observers.has(eventType)) {
            this.observers.set(eventType, new Set());
        }
        this.observers.get(eventType).add(observer);
    }

    unsubscribe(observer, eventType) {
        if (this.observers.has(eventType)) {
            this.observers.get(eventType).delete(observer);
            if (this.observers.get(eventType).size === 0) {
                this.observers.delete(eventType);
            }
        }
    }

    notify(eventType, message) {
        if (this.observers.has(eventType)) {
            // Copy to array to avoid issues if observers change during notification
            const snapshot = Array.from(this.observers.get(eventType));
            snapshot.forEach(observer => observer.update(eventType, message));
        }
    }
}

class ConcreteObserver extends Observer {
    constructor(name) {
        super();
        this.name = name;
    }

    update(eventType, message) {
        console.log(`${this.name} received ${eventType}: ${message}`);
    }
}

// Example usage
const subject = new Subject();
const obs1 = new ConcreteObserver("Observer A");
const obs2 = new ConcreteObserver("Observer B");
subject.subscribe(obs1, "news");
subject.subscribe(obs2, "sports");
subject.notify("news", "News event 1");
subject.unsubscribe(obs1, "news");
subject.notify("news", "News event 2");
Line Notes
this.observers = new Map();Map event types to sets of observers for thread-safe-like behavior in single-threaded JS
const snapshot = Array.from(this.observers.get(eventType));Create snapshot array to avoid mutation during iteration and ensure consistent notifications
snapshot.forEach(observer => observer.update(...))Notify observers from snapshot to prevent errors if list changes during notification
this.observers.get(eventType).delete(observer);Remove observer safely from the set to unsubscribe
Complexity
TimeO(k) per notification where k is number of observers for event type
SpaceO(n) for storing observers plus O(k) for snapshot during notify

Locking ensures thread safety but snapshot copying adds overhead. Notifications happen outside lock to avoid deadlocks and allow concurrent modifications.

💡 Concurrency safety is critical in multi-threaded apps to avoid crashes or missed notifications.
Interview Verdict: Accepted - essential for concurrent environments

This approach is recommended when concurrency is a concern, demonstrating advanced understanding of thread safety in design patterns.

📊
All Approaches - One-Glance Tradeoffs
💡 For most interviews, coding the basic list-based observer pattern is sufficient. Mention improvements and concurrency if asked.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute ForceO(n) per notificationO(n)NoN/ACode this to demonstrate understanding of Observer pattern basics
2. Improved with Event FilteringO(k) per notification (k ≤ n)O(n)NoN/AMention or code if asked about scalability or selective notifications
3. Thread-Safe ImplementationO(k) plus locking overheadO(n) plus snapshot overheadNoN/AMention if concurrency is relevant; code if asked
💼
Interview Strategy
💡 Use this guide to understand the Observer pattern from simple to advanced implementations. Start by explaining the basic subscription-notification mechanism, then discuss improvements and concurrency considerations.

How to Present

Step 1: Clarify the problem and confirm requirements (e.g., multiple observers, dynamic subscription).Step 2: Present the brute force approach with a list of observers and simple notification.Step 3: Discuss limitations and improve with event filtering and efficient data structures.Step 4: If concurrency is relevant, explain thread-safe implementations.Step 5: Write clean, modular code and test with edge cases.

Time Allocation

Clarify: 3min → Approach: 5min → Code: 10min → Test: 5min. Total ~23min

What the Interviewer Tests

The interviewer checks your understanding of decoupling, dynamic subscription management, efficient notification, and optionally thread safety.

Common Follow-ups

  • How would you handle priority observers? → Use priority queues or ordered collections.
  • How to avoid memory leaks with observers? → Use weak references or explicit unsubscribe.
💡 Follow-ups test deeper knowledge of pattern variations and real-world issues like resource management.
🔍
Pattern Recognition

When to Use

1) Multiple objects need to be notified of state changes; 2) Loose coupling between subject and observers is desired; 3) Dynamic subscription/unsubscription is required; 4) Notifications should be automatic and transparent.

Signature Phrases

notify all observerssubscribe/unsubscribe listenersevent-driven updatespublish-subscribe mechanism

NOT This Pattern When

Singleton (only one instance), Strategy (algorithm selection), or Iterator (traversal) patterns

Similar Problems

Mediator Pattern - centralizes communication but differs by controlling interactionsEvent Bus - similar publish-subscribe but often more decoupled and asynchronousCallback Functions - simpler notification but less structured

Practice

(1/5)
1. In designing a parking lot system using OOP, which component is best suited to decide which type of parking spot (e.g., compact, large, handicapped) should be allocated to an incoming vehicle?
easy
A. The ParkingLot class, as it manages all spots and vehicles
B. The ParkingSpot class, since it represents the spot's characteristics
C. The Vehicle class, because it knows its own size and type
D. A Factory or Strategy pattern component that encapsulates the allocation logic

Solution

  1. Step 1: Understand responsibilities

    The Vehicle class only knows about itself, not allocation rules. The ParkingSpot class represents a spot but doesn't decide allocation. The ParkingLot manages overall state but delegating allocation logic to a dedicated component improves modularity.
  2. Step 2: Recognize design pattern role

    The Factory or Strategy pattern encapsulates allocation logic, allowing easy extension and modification without changing core classes.
  3. Final Answer:

    Option D -> Option D
  4. Quick Check:

    Allocation logic centralized -> easier to maintain and extend [OK]
Hint: Allocation logic belongs in a dedicated pattern component, not core entities [OK]
Common Mistakes:
  • Assigning allocation responsibility to Vehicle or ParkingSpot classes
  • Putting all logic inside ParkingLot class leading to tight coupling
2. Consider the following Python singleton implementation using lazy initialization. What will be the output of the code below?
easy
A. false
B. Raises an exception
C. true
D. null

Solution

  1. Step 1: Trace instance creation

    First call to Singleton() triggers __new__, _instance is None, so a new instance is created and assigned to _instance.
  2. Step 2: Trace second instance creation

    Second call to Singleton() triggers __new__, _instance is not None, so the existing instance is returned.
  3. Final Answer:

    Option C -> Option C
  4. Quick Check:

    Both variables point to the same instance, so 'is' returns true [OK]
Hint: 'is' checks identity; singleton returns same instance [OK]
Common Mistakes:
  • Assuming new instance is created each time
  • Confusing 'is' with '=='
  • Expecting null or error due to lazy init
3. Which of the following statements about the Liskov Substitution Principle is INCORRECT?
medium
A. A subclass can strengthen preconditions of an inherited method to ensure better input validation.
B. A subclass must not weaken postconditions of an inherited method.
C. Covariance in return types is allowed under LSP.
D. Contravariance in method parameter types is allowed under LSP.

Solution

  1. Step 1: Recall LSP precondition rule

    Subclasses must not strengthen preconditions; they can only maintain or weaken them.
  2. Step 2: Analyze each statement

    A subclass can strengthen preconditions of an inherited method to ensure better input validation. is incorrect because strengthening preconditions breaks substitutability. A subclass must not weaken postconditions of an inherited method. is correct; subclasses can weaken postconditions. Covariance in return types is allowed under LSP. is correct; covariance in return types is allowed. Contravariance in method parameter types is allowed under LSP. is correct; contravariance in parameter types is allowed.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Strengthening preconditions violates LSP.
Hint: Preconditions can only be weakened, not strengthened, in subclasses.
Common Mistakes:
  • Confusing precondition and postcondition rules
  • Believing strengthening preconditions is safe
  • Misunderstanding covariance and contravariance
4. Examine the following buggy code implementing the Template Method Pattern. Which line contains the subtle bug that breaks the pattern's intended behavior?
medium
A. Line overriding prepare_recipe in Tea subclass
B. Line defining abstract method brew in base class
C. Line calling add_condiments inside prepare_recipe base method
D. Line overriding customer_wants_condiments in Tea subclass

Solution

  1. Step 1: Identify overridden methods

    Tea overrides prepare_recipe, which breaks the template method pattern by duplicating and changing the algorithm flow.
  2. Step 2: Understand impact

    Overriding the template method in subclass bypasses the base class skeleton, causing inconsistent behavior and code duplication.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Template method must not be overridden by subclasses [OK]
Hint: Overriding template method breaks algorithm skeleton [OK]
Common Mistakes:
  • Thinking overriding abstract methods is bug
  • Ignoring hook method usage
5. Consider a subclass that overrides a method with a covariant return type but also changes the method's side effects in a way that violates the superclass's behavioral contract. How should this be addressed to maintain LSP compliance?
hard
A. Allow the side effect changes since the return type is covariant and thus safe.
B. Ignore side effects as they are not part of the method signature and thus irrelevant to LSP.
C. Refactor the subclass to preserve the original side effects or weaken them, ensuring behavioral compatibility.
D. Change the superclass method to accommodate the subclass's side effects.

Solution

  1. Step 1: Understand LSP behavioral contract

    LSP requires that subclasses preserve the observable behavior of the superclass, including side effects.
  2. Step 2: Analyze covariant return type

    Covariant return types are allowed and safe, but side effects must still comply with the superclass contract.
  3. Step 3: Evaluate options

    Refactor the subclass to preserve the original side effects or weaken them, ensuring behavioral compatibility. correctly suggests refactoring to preserve or weaken side effects. Allow the side effect changes since the return type is covariant and thus safe. is incorrect; side effect changes can break clients. Ignore side effects as they are not part of the method signature and thus irrelevant to LSP. is false; side effects are part of behavior and relevant. Change the superclass method to accommodate the subclass's side effects. is risky and breaks superclass abstraction.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Behavioral compatibility includes side effects, not just signatures.
Hint: Covariant returns are safe, but side effects must not violate superclass behavior.
Common Mistakes:
  • Ignoring side effects in LSP
  • Assuming covariant return types cover all behavioral changes
  • Modifying superclass to fit subclass