LLD Cohort · Week 5 · Sat 18 Apr 2026 · recording 2h31m

Observer, State & Chain of Responsibility

Three behavioral patterns: Observer (one change, many listeners), State (an object behaves differently in different states) and Chain of Responsibility (pass a request along until someone handles it). It also compares Strategy and State.

Each topic: one-line idea → the problem → real-life picture → the design step by step → Python code → doubts & corrections → remember

Observer pattern ▶ 1:08:53 – 1:38:49

When one object (the subject) changes, every object that registered interest (the observers) is told about it automatically.

The problem

Many systems have one thing that changes and many things that must react: a stock price changes and the mobile app, the email service and the SMS service must all update. A weather station updates and many screens must refresh. The pain point in one line: one object changes, many objects must react. If the changing object has to know every reacting object by name and call it in its own way, the code gets tightly tied together. Adding a new listener means editing the subject.

Real-life picture

YouTube: Pradeep's channel has many subscribers. When he uploads a video (the event), every subscriber is notified. The channel does not care who you are. It only knows you subscribed.

Zepto "Notify me": milk is out of stock. You press "Notify me", so you are now a subscriber. When milk comes back in stock, all 10 people who pressed the button are notified.

The idea in one line

Observers register with the subject. When the subject's state changes, it loops over its list and calls the same method on each observer. "It is just a fancy way of writing a for loop."

Use it when there is a one-to-many relationship: notifications, events, stock tickers, order status updates, back-in-stock alerts.

Building it step by step

Step 1: the simple YouTube version. The teacher built this from scratch so you can rebuild it even if you forget the pattern. There is a Subscriber interface with one method, notify. Avinash, Ajay and Shekhar each implement it. YouTubeChannel keeps a list of subscribers. upload_video prints a message and then calls a private _notify_subscribers, which loops and calls notify on each one.

Python
from abc import ABC, abstractmethod


class Subscriber(ABC):
    """Anyone who wants to be a subscriber MUST have notify()."""
    @abstractmethod
    def notify(self, video: str) -> None: ...


class Avinash(Subscriber):
    def notify(self, video: str) -> None:
        print(f"Avinash is notified: {video}")


class Ajay(Subscriber):
    def notify(self, video: str) -> None:
        print(f"Ajay is notified: {video}")


class Shekhar(Subscriber):
    def notify(self, video: str) -> None:
        print(f"Shekhar is notified: {video}")


class YouTubeChannel:
    """The subject. It HAS a list of subscribers (aggregation)."""
    def __init__(self, name: str) -> None:
        self.name = name
        self._subscribers: list[Subscriber] = []

    def add_subscriber(self, s: Subscriber) -> None:      # registration
        self._subscribers.append(s)

    def remove_subscriber(self, s: Subscriber) -> None:   # unsubscribe
        self._subscribers.remove(s)

    def _notify_subscribers(self, video: str) -> None:    # private: only the channel calls it
        for s in self._subscribers:
            s.notify(video)          # same method for everyone

    def upload_video(self, video: str) -> None:           # the state change / event
        print(f"Video uploaded: {video}")
        self._notify_subscribers(video)


# usage
channel = YouTubeChannel("Pradeep Kumar")
ajay, shekhar, avinash = Ajay(), Shekhar(), Avinash()
for s in (ajay, shekhar, avinash):
    channel.add_subscriber(s)
channel.upload_video("Observer Pattern")
channel.remove_subscriber(shekhar)          # Shekhar unsubscribed
channel.upload_video("State Pattern")
# Video uploaded: Observer Pattern
# Ajay is notified: Observer Pattern
# Shekhar is notified: Observer Pattern
# Avinash is notified: Observer Pattern
# Video uploaded: State Pattern
# Ajay is notified: State Pattern
# Avinash is notified: State Pattern

Step 2: standardize the subject too. Suppose your app has many subjects: a weather station, a stock market, a YouTube channel. Then give them a common Subject interface with add_observer, remove_observer and notify_observers. The teacher showed a Stock subject from his notes. It holds a price, and set_price notifies the observers. Then he coded a Zomato example the class picked: customers subscribe to "tell me when pizza is available".

Python
from abc import ABC, abstractmethod


class Observer(ABC):
    @abstractmethod
    def notify(self, message: str) -> None: ...


class Subject(ABC):
    """Optional interface. Only needed if you have more than one kind of subject."""
    @abstractmethod
    def add_observer(self, o: Observer) -> None: ...
    @abstractmethod
    def remove_observer(self, o: Observer) -> None: ...
    @abstractmethod
    def notify_observers(self, message: str) -> None: ...


class Zomato(Subject):
    """Observable: customers watch it for item availability."""
    def __init__(self) -> None:
        self._observers: list[Observer] = []

    def add_observer(self, o: Observer) -> None:
        self._observers.append(o)

    def remove_observer(self, o: Observer) -> None:
        self._observers.remove(o)

    def notify_observers(self, message: str) -> None:
        for o in self._observers:
            o.notify(message)

    def item_available(self, item: str) -> None:   # state change
        self.notify_observers(f"{item} is available now!")


class Customer(Observer):
    """In Zomato, 'Customer' reads better than 'Observer'. Names are your choice."""
    def __init__(self, name: str) -> None:
        self.name = name

    def notify(self, message: str) -> None:
        print(f"{self.name} got: {message}")


# usage
zomato = Zomato()
zomato.add_observer(Customer("David"))
zomato.add_observer(Customer("Payal"))
zomato.item_available("Pizza")
# David got: Pizza is available now!
# Payal got: Pizza is available now!
classDiagram
  class Subject {
    +add_observer(o)
    +remove_observer(o)
    +notify_observers(msg)
  }
  class Observer {
    +notify(msg)
  }
  class Zomato {
    -observers
    +item_available(item)
  }
  class Customer {
    +name
    +notify(msg)
  }
  Zomato ..|> Subject
  Customer ..|> Observer
  Zomato o-- Observer

The two phases

  1. Registration phase: an observer registers itself (add_subscriber / add_observer). The subject stores it in its list.
  2. State-change phase: something happens (video uploaded, price changed). The subject calls its internal notify_all, which calls notify() on each observer.
Remember

The Observer interface exists to force every observer to have the same notify() method. If one observer named it zomato_notify(), the subject's for-loop would break. The Subject interface is optional. Create it only when you have several kinds of subjects.

RelationshipWhich oneWhy
YouTubeChannel and its subscribersAggregation (has-a)Subscribers live on their own. The channel does not create or destroy them.
Avinash and SubscriberIs-a (realization)Avinash is a subscriber. He implements the interface.
Common pitfalls:
  • Forgetting to unsubscribe. The subject keeps a reference, so dead objects stay in memory and keep getting calls.
  • One slow or crashing observer can block or break the loop for everyone else. In real systems, wrap each call in a try/except or notify asynchronously.
  • Do not rely on the order of notification.
Java vs Python

Java uses an interface Observer and private List<Observer>. In Python, abc.ABC plays the role of the interface, and a leading underscore (_observers) means "private by convention". Python can also store plain functions (callables) as observers. The class version matches the video and interviews better.

Doubt: Avinash, Ajay and Shekhar implement Subscriber, but they are not registered. How do they get notified?
→

They don't, until you call channel.add_subscriber(...). Implementing the interface only makes them able to be notified. Registering puts them in the list.

Doubt: Is the channel-to-subscriber relationship aggregation or composition?
→

Aggregation. A subscriber can exist without the channel. Composition is also a has-a relationship, but there the part dies with the owner.

Doubt: Should I call it Subject or Observable?
→

Both mean the same thing: the thing being watched. Pick one and be consistent. Use domain names when they read better (Customer instead of Observer).

Key takeaways
  • Observer = one-to-many notification. The subject keeps a list and loops over it.
  • Three subject methods: add, remove, notify. One observer method: notify.
  • The observer interface forces a common method, so the subject never cares who is listening.
  • Subject to observers is aggregation. You add observers without editing the subject (Open-Closed principle).
  • Spot it in interviews: "notify me", subscribers, alerts, price/status changes.

State pattern ▶ 1:39:40 – 2:25:30

An object changes its behavior (methods) when its internal state changes. It does this by handing each call to a separate state object.

First, two words: state and behavior

The State pattern says: the same method should behave differently depending on the current state.

Real-life picture

Jira ticket: every button (open, mark done, close) is always visible. But if the ticket is "In Progress" and you click "Done", Jira says "not in the proper state". "Done" works only from "Tested". Same button, different result, depending on the current state.

Elevator: while the lift is moving up, pressing "door open" does nothing. The open-door code exists, but it is disabled in the "moving" state. (Old grille-door lifts did this with electric current. Today it is done in code.)

ATM: with no card inside, "withdraw" says "insert card first". With a card inside, "withdraw" dispenses cash. Tip from the teacher: most machines (ATM, vending machine, elevator, coffee machine) use the State pattern.

The problem: the if-else version

Without the pattern, one class holds a state variable, and every method has an if-else ladder:

Python
class ATMMachine:                       # BAD: every method checks every state
    def __init__(self) -> None:
        self.state = "NO_CARD"

    def insert_card(self) -> None:
        if self.state == "NO_CARD":
            print("Card inserted."); self.state = "HAS_CARD"
        elif self.state == "HAS_CARD":
            print("Card already inserted.")
        # ...add a new state? edit EVERY method again

    def withdraw_cash(self, amount: int) -> None:
        if self.state == "NO_CARD":
            print("Insert the card first.")
        elif self.state == "HAS_CARD":
            print(f"Dispensing {amount}.")

Problems: every new state means editing every method, which violates the Open-Closed principle (open for extension, closed for modification). The class gets huge and tightly coupled. "Design patterns hate if-else."

The idea in one line

Take the methods out of the big class. Write one class per state, and each one implements all the methods for that state. The main object (the context, here the ATM) keeps a current_state and just forwards every call to it. From the outside it looks as if the object "changes its class at runtime".

Use it when an object has clear states, a fixed set of actions (buttons), and rules for which action is allowed in which state.

Building it step by step (ATM)

  1. List the states: NoCard, HasCard (and CashDispensing on the whiteboard).
  2. List the actions the public can perform, i.e. the buttons: insert card, eject card, withdraw cash. "You can't trust users. They will press any button, so you must control every one."
  3. Make a behavior table: for each state and action, decide what should happen.
  4. Create an ATMState interface with those actions. Write one class per state.
  5. Each state keeps a reference to the ATM, so it can transition the machine: after "eject card", HasCardState calls atm.set_state(atm.no_card_state). The state is responsible for its own transition. "Nobody else will come to save you."
  6. The ATM is "a big dummy": every public method just calls self.current_state.same_method().
Action \ StateNo cardCard insertedCash dispensing
Insert cardYes, card acceptedNo, already has cardNo
Withdraw cashNo, insert card firstYesNo, already dispensing
Cancel transactionNoYesNo
Python
from __future__ import annotations
from abc import ABC, abstractmethod


class ATMState(ABC):
    """All the 'buttons' of the ATM. Every state must say how each behaves."""
    def __init__(self, atm: ATMMachine) -> None:
        self.atm = atm              # needed to change the machine's state

    @abstractmethod
    def insert_card(self) -> None: ...
    @abstractmethod
    def eject_card(self) -> None: ...
    @abstractmethod
    def withdraw_cash(self, amount: int) -> None: ...


class NoCardState(ATMState):
    def insert_card(self) -> None:
        print("Card inserted successfully.")
        self.atm.set_state(self.atm.has_card_state)    # transition

    def eject_card(self) -> None:
        print("No card to eject.")

    def withdraw_cash(self, amount: int) -> None:
        print("Insert the card first.")


class HasCardState(ATMState):
    def insert_card(self) -> None:
        print("Card already inserted.")

    def eject_card(self) -> None:
        print("Card ejected.")
        self.atm.set_state(self.atm.no_card_state)     # transition

    def withdraw_cash(self, amount: int) -> None:
        print(f"Dispensing {amount}.")


class ATMMachine:
    """Context. Knows all its states, holds the current one, forwards every call."""
    def __init__(self) -> None:
        self.no_card_state = NoCardState(self)       # created once, reused
        self.has_card_state = HasCardState(self)
        self.current_state: ATMState = self.no_card_state

    def set_state(self, state: ATMState) -> None:
        self.current_state = state

    # public buttons: no logic here, just delegate
    def insert_card(self) -> None:
        self.current_state.insert_card()

    def eject_card(self) -> None:
        self.current_state.eject_card()

    def withdraw_cash(self, amount: int) -> None:
        self.current_state.withdraw_cash(amount)


# usage
atm = ATMMachine()
atm.withdraw_cash(500)   # Insert the card first.
atm.insert_card()        # Card inserted successfully.
atm.insert_card()        # Card already inserted.
atm.withdraw_cash(500)   # Dispensing 500.
atm.eject_card()         # Card ejected.
atm.eject_card()         # No card to eject.
classDiagram
  class ATMMachine {
    -current_state
    +set_state(state)
    +insert_card()
    +eject_card()
    +withdraw_cash(amount)
  }
  class ATMState {
    +insert_card()
    +eject_card()
    +withdraw_cash(amount)
  }
  class NoCardState
  class HasCardState
  NoCardState ..|> ATMState
  HasCardState ..|> ATMState
  ATMMachine *-- ATMState
  ATMState --> ATMMachine

Second example: traffic light (and reusing state objects)

A traffic light has Red, Green and Yellow states. Each state has change() (go to the next colour) and describe(). Red changes to green, green to yellow, yellow to red. The context (TrafficLight) plays the same role as the ATM.

The issue: the textbook code does context.set_state(GreenLight()), which creates a new object on every change. State objects hold no per-change data, so they can be reused. Fix: create each light once and keep it in a factory with a hash map. The teacher said this exact cross-question was asked in a MoveInSync interview ("how will you reuse the objects?"), and the candidate missed it.

Python
from __future__ import annotations
from abc import ABC, abstractmethod


class TrafficLightState(ABC):
    @abstractmethod
    def change(self, light: TrafficLight) -> None: ...
    @abstractmethod
    def describe(self) -> str: ...


class RedLight(TrafficLightState):
    def change(self, light: TrafficLight) -> None:
        light.set_state(LightFactory.get("GREEN"))
    def describe(self) -> str:
        return "RED: stop"


class GreenLight(TrafficLightState):
    def change(self, light: TrafficLight) -> None:
        light.set_state(LightFactory.get("YELLOW"))
    def describe(self) -> str:
        return "GREEN: go"


class YellowLight(TrafficLightState):
    def change(self, light: TrafficLight) -> None:
        light.set_state(LightFactory.get("RED"))
    def describe(self) -> str:
        return "YELLOW: slow down"


class LightFactory:
    """Creates each state once, then hands out the same object (hash map cache)."""
    _cache: dict[str, TrafficLightState] = {}
    _types = {"RED": RedLight, "GREEN": GreenLight, "YELLOW": YellowLight}

    @classmethod
    def get(cls, color: str) -> TrafficLightState:
        if color not in cls._cache:
            cls._cache[color] = cls._types[color]()
        return cls._cache[color]


class TrafficLight:
    """Context: just forwards to the current state."""
    def __init__(self) -> None:
        self._state = LightFactory.get("RED")

    def set_state(self, state: TrafficLightState) -> None:
        self._state = state

    def change(self) -> None:
        self._state.change(self)

    def describe(self) -> str:
        return self._state.describe()


# usage
light = TrafficLight()
for _ in range(4):
    print(light.describe())
    light.change()
print(LightFactory.get("RED") is LightFactory.get("RED"))
# RED: stop
# GREEN: go
# YELLOW: slow down
# RED: stop
# True   (same object reused, nothing new created)
Remember

Context = "router". State classes = the real code for each situation. The state holds a reference back to the context so it can change the context's state. Adding a new state means adding a new class. No existing method grows another elif.

Common mistakes:
  • Putting transition logic in the client. The state decides the next state.
  • Creating a new state object on every transition. Create them once (in the context, or in a factory map) and reuse them.
  • Forgetting a button in some state. Every state must implement every action, even if it only prints "not allowed".
Java vs Python

Java needs the state interface and context class to refer to each other. That just compiles. In Python, a type hint that uses a class defined later needs from __future__ import annotations (or quotes, "ATMMachine"). Java's private ATMState noCardState becomes a normal attribute in Python. The underscore prefix is only a convention.

Doubt: What exactly is ATMState? Where is it defined?
→

It is the interface that lists every action you can perform on the ATM (insert card, eject card, withdraw cash). Each state class implements it and says how the machine responds to that button in that state.

Doubt: Can't we just use an enum for the states? It looks simpler.
→

An enum only names the states. You would still need if-else in every method to pick the behavior. In the State pattern, each state carries its own code, and that code can be a thousand lines. Use an enum when states have no behavior. Use state classes when the behavior differs per state.

Doubt: How many states can there be?
→

Any number. Each one is just another class that implements the interface.

Doubt: Why does each state need a reference to the ATM?
→

To transition. After ejecting the card, HasCardState must move the machine to NoCardState: atm.set_state(atm.no_card_state). Also, the real work (actually dispensing cash) is done by the machine, and the state only decides whether it is allowed.

Homework: design a vending machine using the State pattern.

Key takeaways
  • State pattern = same method, different behavior depending on the current state, with no if-else ladders.
  • Recipe: list the states, list the actions, make an interface of the actions, write one class per state, and have the context delegate to current_state.
  • States trigger their own transitions through a reference to the context.
  • Reuse state objects. Create them once or cache them in a factory map (a common interview cross-question).
  • Think "machines": ATM, vending machine, elevator, traffic light, Jira workflow.

Strategy vs State ▶ recap 1:03:00 · 2:25:30

Both have one interface with many implementing classes. The difference is who picks the class and whether it changes by itself.

Quick recap of Strategy (last class): one problem, many approaches to solve it. Each approach is a class that implements the same interface. The context uses whichever one it was given, and you can swap it without touching the client. Sorting strategies, URL-shortener strategies and elevator-dispatch strategies are examples.

StrategyState
Question it answersHow should I do this task?What am I allowed to do right now, and how?
Who chooses the classThe client (from outside)The states themselves, as things happen
Does it change by itself?No. It stays until someone swaps it.Yes. Every action may move to the next state.
Do the classes know each other?No, they are independentYes, a state knows which state comes next
Reference to the context?Usually not neededYes, needed to call set_state
ExamplePay by card / UPI / cash. Sort by price / rating.ATM: NoCard, then HasCard, then NoCard. Traffic light: Red, Green, Yellow.
Simple test"Pick one way of doing the job.""The object has a lifecycle."
Why both are "behavioral" patterns

Behavior means methods. A behavioral pattern controls which method runs. Strategy: pick an approach. State: pick by the current situation. Chain of Responsibility: try this handler, then pass to the next one.

Chain of Responsibility ▶ 2:25:43 – 2:41:45

Pass a request along a chain of handlers. Each handler either processes it or passes it to the next one.

The problem

Some requests need escalation. A support ticket may be fixable by Level 1. If not, it goes to Level 2, then Level 3. If the client had to decide who handles what, it would need to know every level and its rules. We want the client to hand the ticket to the first desk and forget about it.

Real-life picture

IT ticket / customer service: you raise a ticket and it is assigned to person A. If they can't fix it, they reassign it to the next team. It is not your job to find the right team. Likewise, the front desk passes you to a supervisor, who passes you to a manager.

The idea in one line

Each handler holds a reference to the next handler, like a linked list (data plus a next pointer). The request flows left to right until someone can handle it or the chain ends.

Use it when there is a chain of possible handlers: escalation levels, logging levels (the teacher will use it in next class's Logger design), approval workflows, middleware/filters.

Building it step by step

  1. Without any pattern: there are requests with a level, and handlers. If the level matches, a handler does the work. Otherwise it passes the request on. To pass it on, every handler needs the same method name (handle_request). Otherwise handler 1 can't know what to call on handler 2. That is the real reason for a common interface.
  2. Use an abstract class, not a pure interface. Every handler needs a next_handler field and the same "can I handle? else pass on" logic. Putting these in the base class avoids repeating them in each level. (A Java interface can't hold instance fields.)
  3. Split the methods: the public handle_request holds the common flow. The abstract can_handle and process are what each level fills in.
  4. Wire the chain from outside with set_next_handler. The client always calls Level 1 only.
Python
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional


@dataclass
class SupportTicket:
    issue: str
    priority: int          # 1 = easy ... 3 = hard


class SupportHandler(ABC):
    """Base class: holds the 'next' link and the common pass-along logic."""
    def __init__(self) -> None:
        self._next_handler: Optional[SupportHandler] = None

    def set_next_handler(self, nxt: SupportHandler) -> None:
        self._next_handler = nxt

    def handle_request(self, ticket: SupportTicket) -> None:
        if self.can_handle(ticket):
            self.process(ticket)
        elif self._next_handler is not None:
            print(f"{type(self).__name__} cannot handle, passing to next handler")
            self._next_handler.handle_request(ticket)
        else:
            print(f"No handler available for: {ticket.issue}")

    @abstractmethod
    def can_handle(self, ticket: SupportTicket) -> bool: ...

    @abstractmethod
    def process(self, ticket: SupportTicket) -> None: ...


class Level1Support(SupportHandler):
    def can_handle(self, t: SupportTicket) -> bool:
        return t.priority <= 1
    def process(self, t: SupportTicket) -> None:
        print(f"Level 1 resolved: {t.issue}")


class Level2Support(SupportHandler):
    def can_handle(self, t: SupportTicket) -> bool:
        return t.priority <= 2
    def process(self, t: SupportTicket) -> None:
        print(f"Level 2 resolved: {t.issue}")


class Level3Support(SupportHandler):
    def can_handle(self, t: SupportTicket) -> bool:
        return t.priority <= 3
    def process(self, t: SupportTicket) -> None:
        print(f"Level 3 resolved: {t.issue}")


# usage: build the chain L1 -> L2 -> L3, always start at L1
level1, level2, level3 = Level1Support(), Level2Support(), Level3Support()
level1.set_next_handler(level2)
level2.set_next_handler(level3)

level1.handle_request(SupportTicket("Login issue", 1))
level1.handle_request(SupportTicket("Server down", 3))
level1.handle_request(SupportTicket("Data centre fire", 5))
# Level 1 resolved: Login issue
# Level1Support cannot handle, passing to next handler
# Level2Support cannot handle, passing to next handler
# Level 3 resolved: Server down
# Level1Support cannot handle, passing to next handler
# Level2Support cannot handle, passing to next handler
# No handler available for: Data centre fire
classDiagram
  class SupportHandler {
    -next_handler
    +set_next_handler(nxt)
    +handle_request(ticket)
    +can_handle(ticket)
    +process(ticket)
  }
  class SupportTicket {
    +issue
    +priority
  }
  class Level1Support
  class Level2Support
  class Level3Support
  Level1Support --|> SupportHandler
  Level2Support --|> SupportHandler
  Level3Support --|> SupportHandler
  SupportHandler o-- SupportHandler : next
  SupportHandler --> SupportTicket
Remember

The client only knows the first handler. Each handler only knows the next one. handle_request is the same for everyone and lives in the base class. Only can_handle and process differ.

Common pitfalls:
  • Always handle the end of the chain ("no handler available"). Otherwise the request silently disappears.
  • Wire the chain in the right order, and never let it loop back on itself.
  • The teacher called this full version "peak" code. A simpler variant where each level hard-codes its own if/else also works. Start simple if needed, but aim for this shape.
Java vs Python

Java used protected SupportHandler nextHandler in an abstract class, because interface fields would be public static final. Python has no protected, so _next_handler signals "for subclasses only".

Doubt: Which data structure does this look like?
→

A linked list. Each handler is a node with a next pointer, and the last one points to nothing (None).

Doubt: Why an abstract class instead of an interface?
→

All handlers share a field (next_handler) and the same pass-along logic. An abstract class stores them once. With an interface, every level would repeat that code.

Key takeaways
  • Chain of Responsibility = try to handle, otherwise pass to the next handler.
  • Handlers form a linked list. The client talks only to the first one.
  • The abstract base class holds next_handler plus handle_request. Subclasses implement can_handle and process.
  • Used for escalations, logging levels (next class: Logger) and approval chains.