Bird
Raised Fist0
Interview Prepoop-design-patternsmediumAmazonGoogleMicrosoftFlipkartSwiggyRazorpayCRED

Observer Pattern - Event System, Publish-Subscribe

Choose your preparation mode3 modes available
🎯
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. When an object composed of multiple behaviors receives a request to perform an action, what is the typical sequence of internal steps that occur to fulfill this request?
easy
A. The object delegates the action to one or more composed behavior objects which execute their respective parts
B. The object checks flags internally and runs conditional code for each behavior
C. The object directly executes the action code inherited from its superclass
D. The object creates a new subclass instance dynamically to handle the action

Solution

  1. Step 1: Understand delegation in composition

    In composition, the main object delegates responsibilities to composed behavior objects rather than handling all logic itself.
  2. Step 2: Analyze each option

    The object directly executes the action code inherited from its superclass describes inheritance, not composition. The object checks flags internally and runs conditional code for each behavior implies flag-based conditional logic, which is less flexible. The object creates a new subclass instance dynamically to handle the action is not a typical or practical approach.
  3. Step 3: Confirm correct flow

    The composed behaviors receive the delegated call and execute their specific logic, enabling modular and maintainable design.
  4. Final Answer:

    Option A -> Option A
  5. Quick Check:

    Delegation to composed objects is the hallmark of composition-based design.
Hint: Composition means delegation to behavior objects, not direct inheritance execution.
Common Mistakes:
  • Confusing inheritance method calls with composition delegation
  • Assuming flags control behavior execution internally
2. Trace the sequence of method resolution when a subclass object calls an overridden method via a superclass reference variable. What happens step-by-step internally?
easy
A. The compiler binds the method call to the superclass method at compile time, and the superclass method executes at runtime
B. The compiler generates multiple versions of the method for each subclass, and the correct one is chosen at compile time
C. The compiler defers binding until runtime, where the object's actual class method is looked up via the vtable and executed
D. The method call is resolved by matching the method signature with the parameter types at runtime

Solution

  1. Step 1: Compile-time binding

    The compiler knows the reference type but defers binding for overridden methods to runtime.
  2. Step 2: Runtime dispatch

    At runtime, the actual object's class is identified, and the method pointer is retrieved from the vtable.
  3. Step 3: Method execution

    The overridden method in the subclass is executed, enabling dynamic polymorphism.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Overriding uses dynamic dispatch via vtable lookup at runtime, not compile-time binding.
Hint: Overriding = runtime binding via vtable lookup
Common Mistakes:
  • Assuming compile-time binding for overridden methods
  • Confusing overloading resolution with overriding
  • Believing method signature matching happens at runtime
3. Why might using an abstract class instead of an interface limit flexibility in a large-scale system design?
medium
A. Because abstract classes do not support polymorphism.
B. Because abstract classes cannot define method signatures without implementation.
C. Because a class can only extend one abstract class but can implement multiple interfaces.
D. Because abstract classes force all methods to be implemented immediately.

Solution

  1. Step 1: Recall inheritance limitations

    Most languages allow single inheritance of classes, limiting abstract class extension to one per class.
  2. Step 2: Interface multiple implementation

    Classes can implement multiple interfaces, allowing flexible contracts from various sources.
  3. Step 3: Why other options are incorrect

    Abstract classes can define abstract methods (method signatures), support polymorphism, and do not force immediate implementation if abstract.
  4. Final Answer:

    Option C -> Option C
  5. Quick Check:

    Single inheritance limits abstract class use; interfaces allow multiple contracts.
Hint: Abstract classes limit inheritance; interfaces allow multiple implementations.
Common Mistakes:
  • Believing abstract classes cannot have abstract methods.
  • Thinking abstract classes do not support polymorphism.
  • Assuming abstract classes force immediate method implementation.
4. What is the time complexity of constructing a complex object using the Builder pattern when the object has k parts to build?
medium
A. O(k), because each of the k parts is built sequentially
B. O(1), since each build method is called once
C. O(k^2), due to nested calls between builder methods
D. O(log k), as parts are built using divide-and-conquer

Solution

  1. Step 1: Identify number of build steps

    The Builder pattern calls a build method for each part, so k parts mean k method calls.
  2. Step 2: Analyze time per build call

    Each build method adds a part in O(1) time, so total time is O(k).
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Linear time proportional to number of parts built [OK]
Hint: Each part built once -> O(k) time [OK]
Common Mistakes:
  • Confusing constant time with O(k)
  • Assuming nested calls cause O(k^2)
5. Suppose you want to implement a prototype pattern for an object that contains references to other objects which themselves may reference back to the original object (cyclic references). Which approach correctly handles deep copying in this scenario?
hard
A. Use a deep copy implementation with memoization to track already copied objects and avoid infinite recursion
B. Use a naive recursive deep copy without memoization, which will eventually copy all objects
C. Use shallow copy to avoid recursion issues, accepting shared nested references
D. Serialize the object to JSON and deserialize it, which naturally handles cyclic references

Solution

  1. Step 1: Understand cyclic references problem

    Naive recursion without tracking copied objects causes infinite recursion on cycles.
  2. Step 2: Evaluate solutions

    Memoization tracks already copied objects, preventing infinite loops and ensuring correct deep copy. Shallow copy shares references, breaking independence. JSON serialization cannot handle cycles and will fail.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Memoization prevents infinite recursion in cyclic graphs [OK]
Hint: Memoization is essential for deep copying cyclic object graphs [OK]
Common Mistakes:
  • Ignoring cycles causes infinite recursion or stack overflow
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard