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.
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.
- Subject (also called Observable, "something that can be observed"): the object whose state changes. It holds the list of observers.
- Observer (subscriber): someone watching the subject. Every observer has one common method, such as
notify().
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.
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 PatternStep 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".
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
- Registration phase: an observer registers itself (
add_subscriber/add_observer). The subject stores it in its list. - State-change phase: something happens (video uploaded, price changed). The subject calls its internal
notify_all, which callsnotify()on each observer.
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.
| Relationship | Which one | Why |
|---|---|---|
| YouTubeChannel and its subscribers | Aggregation (has-a) | Subscribers live on their own. The channel does not create or destroy them. |
| Avinash and Subscriber | Is-a (realization) | Avinash is a subscriber. He implements the interface. |
- 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 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.
→
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.
→
Aggregation. A subscriber can exist without the channel. Composition is also a has-a relationship, but there the part dies with the owner.
→
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).
- 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
- State = the object's member variables. For a car: speed, gear, fuel amount.
- Behavior = the methods. For a car: start, stop, accelerate. A method is code that works with the state.
The State pattern says: the same method should behave differently depending on the current state.
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:
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)
- List the states: NoCard, HasCard (and CashDispensing on the whiteboard).
- 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."
- Make a behavior table: for each state and action, decide what should happen.
- Create an
ATMStateinterface with those actions. Write one class per state. - 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." - The ATM is "a big dummy": every public method just calls
self.current_state.same_method().
| Action \ State | No card | Card inserted | Cash dispensing |
|---|---|---|---|
| Insert card | Yes, card accepted | No, already has card | No |
| Withdraw cash | No, insert card first | Yes | No, already dispensing |
| Cancel transaction | No | Yes | No |
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.
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)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.
- 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 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.
→
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.
→
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.
→
Any number. Each one is just another class that implements the interface.
→
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.
- 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.
| Strategy | State | |
|---|---|---|
| Question it answers | How should I do this task? | What am I allowed to do right now, and how? |
| Who chooses the class | The 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 independent | Yes, a state knows which state comes next |
| Reference to the context? | Usually not needed | Yes, needed to call set_state |
| Example | Pay 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." |
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.
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
- 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. - Use an abstract class, not a pure interface. Every handler needs a
next_handlerfield 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.) - Split the methods: the public
handle_requestholds the common flow. The abstractcan_handleandprocessare what each level fills in. - Wire the chain from outside with
set_next_handler. The client always calls Level 1 only.
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 fireclassDiagram
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
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.
- 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 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".
→
A linked list. Each handler is a node with a next pointer, and the last one points to nothing (None).
→
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.
- 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_handlerplushandle_request. Subclasses implementcan_handleandprocess. - Used for escalations, logging levels (next class: Logger) and approval chains.